教材2.6节主要是类与对象的python手搓实现,课程没讲我也看得头大,所以就跳过了……

对象抽象(Object Abstraction)

  • 在面向对象编程的对象系统中,使用了“对象抽象”这一思想(即将数据与操作方法统一为一个整体,通过调用对象的方法实现功能),在函数抽象和数据抽象的基础上更进一层。
  • 而本节我们将介绍“对象抽象”的一个核心概念——泛型函数(generic function),其与普通函数的区别在于可以接收不同数据类型的值,这也是对象抽象的重要基础。

字符串转换(String Conversion)

  • 在对象系统中,我们希望对象的值与数据本身具有相同的行为(比如用print()能够输出数据值);
    而在python的IDLE交互中,数据值/表达式值/函数等都是用字符串的形式展示的。
  • 为什么使用字符串?因为字符串可以在屏幕/纸质媒介上显示,与人类的语言文字最为接近,易于传播(包括编程语言代码本身也以字符串形式展现)
  • python规定所有的对象都应该生成两种字符串:
    1. 一种是人类可读的字符串,用str函数返回;
    2. 一种是python可解释的表示,可以用repr函数查看。repr函数的docstring如下:
      >>> help(repr)
      Help on built-in function repr in module __builtin__:
      
      repr(...)
          repr(object) -> string
          
          Return the canonical string representation of the object.
          For most object types, eval(repr(object)) == object.
  • 在python的IDLE中使用repr()函数就是交互打印的内容:
    >>> 1.68e12
    1680000000000.0
    >>> print(repr(1.68e12))
    1680000000000.0
    >>> repr(min)
    '<built-in function min>'
    当函数内的表达式无法直接求值时,函数会返回一个尖括号包围的对表达式的描述。
  • str函数输出内容与repr类似,只会在某些对象上输出更加容易解释的表示:
    import datetime
    d = datetime.date(2026, 3, 25)
    print(str(d))   # 2026-03-25
    print(repr(d))  # datetime.date(2026, 3, 25)
  • 注意到,这里repr()函数内可以包括所有的数据类型,甚至是调用时还未存在的数据类型。
    这是如何实现的呢?这里我们就可以使用对象系统提供的方法——在类中定义__repr__方法。
    class Rational:
    def __init__(self, num, den):
        self.num, self.den = num, den
    def __repr__(self):
        return f"Rational({self.num}, {self.den})"  
    
    r = Rational(3, 4)
    这样在调用时就可以使用点表达式或使用repr()
    >>> r.__repr__()
    Rational(3, 4)
    >>> repr(r)
    Rational(3, 4)
    类似地我们也可以定义__str__方法并调用。
  • 这种函数也被称为多态函数(polymorphic function),它是泛型函数的一种具体实现。

F-string

  • 这里再补充一个python中打印的方法——F-string。其功能为在字符串中插入表达式,使得在输出时先计算表达式的值,再插入字符串中一并输出。示例:
    >>> print('2 + 2 = 2 + 2')
    '2 + 2 = 2 + 2'
    >>> print(f'2 + 2 = {2 + 2}')
    '2 + 2 = 4'
  • 当然,在大括号内也可以加入变量、函数等(只要能得到返回值即可)。实际上,python处理这种表达式时相当于对值调用了str()函数。

专用方法(Special Methods)

  • 除了__repr__(IDLE显示值时调用)、__str__(print时调用)和之前提到的__init__(创建对象时调用),python中还有一些专用的方法名称,具体如下:
  1. 布尔(bool)判断
    python中所有对象都可以返回布尔值(TrueFalse)。而用户自定义的对象布尔值默认为True,但也可以通过专门的__bool__方法进行修改。
    如之前的bank account例子:
    Account.__bool__ = lambda self: self.balance != 0
    重新定义方法后,我们就可以调用bool()函数判断对象的真假:
    >>> bool(Account('Iroha'))
    False
    >>> if not Account('Iroha'):
            print('Iroha has nothing')
    Iroha has nothing
  2. 序列操作(长度、索引)
    以字符串序列为例,可以通过len()直接得到序列的长度,其背后原理是序列对象中定义了__len__()方法:
    >>> len('Go Iroha!')
    9 
    >>> 'Go Iroha!'.__len__()
    9
    另外,序列的__bool__方法同样基于序列长度定义——当序列长度为00,则返回False,否则返回True
    >>> bool('')
    False
    >>> bool([])
    False
    >>> bool('Go Iroha!')
    True
    而序列取索引则由__getitem__方法实现:
    >>> 'Go Iroha!'[3]
    'I'
    >>> 'Go Iroha!'.__getitem__(3)
    'I'
  3. 可调用对象
    实际上,我们还可以将对象抽象与函数抽象进行结合。在python中,函数既可以传递数据,也可以拥有属性(如高阶函数)。
    我们可以在对象系统中定义__call__方法实现类似高阶函数的功能:
    >>> class Adder(object):
        def __init__(self, n):
            self.n = n
        def __call__(self, k):
            return self.n + k
    >>> add_three_obj = Adder(3)
    >>> add_three_obj(4)
    7
    其等价于下面的高阶函数:
    >>> def make_adder(n):
            def adder(k):
                return n + k
            return adder
    >>> add_three = make_adder(3)
    >>> add_three(4)
    7
  4. 算术运算
    我们还可以在自己定义的类(对象)中定义基本运算(加减乘除)的方法(如__add__等,详见Emulating numeric types)。
    python在计算含有+,-,*,/等的基本表达式时,会检查两边的对象是否有对应的方法,然后调用方法,对返回值进行计算。

更多专用方法可参考:Special method names

多重表示(Multiple Representation)

  • 在之前的抽象屏障中,我们实现了数据表示与数据使用的分离。然而,在一些大型程序中,我们往往希望数据对象能够接收多种数据的表示形式。
    比如,复数可以通过“实部+虚部”表示,也可以通过极坐标形式(模长与辐角)表示。一个完善的复数系统应该能够同时支持处理这两种数据的表示,同时不影响其运算。
  • python内置的complex()已经实现了这一点,下面我们尝试手动实现。

复数系统的构建

  • 我们从最顶层的抽象结构开始搭建。先定义一个Number类,并在其中定义__add____mul__两个通用方法:
    >>> class Number:
            def __add__(self, other):
                return self.add(other)
            def __mul__(self, other):
                return self.mul(other)
  • 接下来我们设计一个Complex类(继承自Number类),定义其加法与乘法两个方法:
    >>> class Complex(Number):
        def add(self, other):
            return ComplexRI(self.real + other.real, self.imag + other.imag) # 实部与虚部分别相加
        def mul(self, other):
            magnitude = self.magnitude * other.magnitude
            return ComplexMA(magnitude, self.angle + other.angle) # 模长相乘,辐角相加
    其中add方法使用了realimag两个参数,而mul则使用了magnitudeangle两个参数。这里假定已经有两个复数表示类:
    • ComplexRI使用实部和虚部构建一个复数。
    • ComplexMA使用模长和辐角构建一个复数。
  • 注意到,这里其实隐式定义了一种数据传输的接口:realimagmagnitudeangle这四个参数。Complex类需要同时掌握这四个参数才能进行运算。
    因而对于每个复数对象而言,其两对参数(属性)必须表示同一个值。然而,如果在对象中直接同时存储这四个属性,那么改变一对属性值就需要同时更改另外两个属性值,这比较麻烦。
  • 一种比较好的方法是对象只存储一对属性的值,再根据需要实时计算另一对属性值。python中可以通过修饰符@property做到这一点:
    >>> from math import atan2
    >>> class ComplexRI(Complex):
            def __init__(self, real, imag):
                self.real = real
                self.imag = imag
            @property
            def magnitude(self):
                return (self.real ** 2 + self.imag ** 2) ** 0.5
            @property
            def angle(self):
                return atan2(self.imag, self.real)
            def __repr__(self):
                return 'ComplexRI({0:g}, {1:g})'.format(self.real, self.imag)
    @property的作用是将下面定义的函数(方法)转换为零参数函数,这样在调用时形式就和属性一样了。示例:
    >>> ri = ComplexRI(5, 12)
    >>> ri.real
    5
    >>> ri.magnitude
    13.0
    >>> ri.real = 9
    >>> ri.real
    9
    >>> ri.magnitude
    15.0
    我们也可以类似定义ComplexMA
    >>> from math import sin, cos, pi
    >>> class ComplexMA(Complex):
        def __init__(self, magnitude, angle):
            self.magnitude = magnitude
            self.angle = angle
            @property
        def real(self):
            return self.magnitude * cos(self.angle)
        @property
        def imag(self):
            return self.magnitude * sin(self.angle)
        def __repr__(self):
            return 'ComplexMA({0:g}, {1:g} * pi)'.format(self.magnitude, self.angle/pi)
  • 这样我们就完整实现了复数系统。在实际运算中,我们可以将ComplexRIComplexMA实例化的对象放在一起运算(因为它们继承自同一个类):
    >>> from math import pi
    >>> ComplexRI(1, 2) + ComplexMA(2, pi/2)
    ComplexRI(1, 4)
    >>> ComplexRI(0, 1) * ComplexRI(0, 1)
    ComplexMA(1, 1 * pi)
    而且这种接口的可变性也很高:如果需要增加一种表示形式,只需要再继承后定义一个新的类,使用共享的属性名即可。可以说,多重表示结合数据抽象大大增加了程序的灵活性。

泛型函数(Generic Function)

  • 终于来到我们本节的最大主题。前面我们已经通过共享接口实现了泛型函数(如Complex.add),下面我们再考虑另外两种方法。

类型分派(type dispatching)

  • 在之前的章节中,我们实现了有理数的表示与运算,这里我们将其改造为面向对象的形式:
    >>> from fractions import gcd
    >>> class Rational(Number):
            def __init__(self, numer, denom):
                g = gcd(numer, denom)
                self.numer = numer // g
                self.denom = denom // g
            def __repr__(self):
                return 'Rational({0}, {1})'.format(self.numer, self.denom)
            def add(self, other):
                nx, dx = self.numer, self.denom
                ny, dy = other.numer, other.denom
                return Rational(nx * dy + ny * dx, dx * dy)
            def mul(self, other):
                numer = self.numer * other.numer
                denom = self.denom * other.denom
                return Rational(numer, denom)
    这样我们就能对有理数(分数形式)进行相加或相乘。然而,我们却无法将有理数与复数相加。一个直接的想法是设计一个通用的__add__方法接收不同类型的数据,但这与我们模块化设计的思想相违背。
  • 于是我们换一种思路:写一个能够检查其所收到的参数类型的函数,然后根据参数类型执行恰当的代码。python中提供了isinstance函数,用于判断对象是否属于特定的类:
    >>> c = ComplexRI(1, 1)
    >>> isinstance(c, ComplexRI)
    True
    >>> isinstance(c, Complex)
    True
    >>> isinstance(c, ComplexMA)
    False
    基于这个函数,我们可以简单写一个判断复数是否为实数的方法:
    >>> def is_real(c):
        """Return whether c is a real number with no imaginary part."""
        if isinstance(c, ComplexRI):
            return c.imag == 0
        elif isinstance(c, ComplexMA):
            return c.angle % pi == 0
    
    >>> is_real(ComplexRI(1, 1))
    False
    >>> is_real(ComplexMA(2, pi))
    True
    不过在类型分派中,我们一般不会使用isinstance,而在每个类中增加一个属性type_tag。比如:
    >>> Rational.type_tag = 'rat'
    >>> Complex.type_tag = 'com'
    >>> Rational(2, 5).type_tag == Rational(1, 2).type_tag
    True
    >>> ComplexRI(1, 1).type_tag == ComplexMA(2, pi/2).type_tag
    True
    >>> Rational(2, 5).type_tag == ComplexRI(1, 1).type_tag
    False
    如果两个对象的type_tag相同,我们就可以直接用x.add(y)处理。而如果type_tag不同,我们就得另外定义相加(与相乘)的方法:
    >>> def add_complex_and_rational(c, r):
        return ComplexRI(c.real + r.numer/r.denom, c.imag)
    >>> def mul_complex_and_rational(c, r):
        r_magnitude, r_angle = r.numer/r.denom, 0
        if r_magnitude < 0:
            r_magnitude, r_angle = -r_magnitude, pi
        return ComplexMA(c.magnitude * r_magnitude, c.angle + r_angle)
    >>> def add_rational_and_complex(r, c): # 交换参数顺序后直接套用上面的方法
        return add_complex_and_rational(c, r)
    >>> def mul_rational_and_complex(r, c):
            return mul_complex_and_rational(c, r)
  • 下面我们来正式重写Number类的内容,对__add____mul__方法增加类型分派:
    >>> class Number:
        def __add__(self, other):
            if self.type_tag == other.type_tag:
                return self.add(other)
            elif (self.type_tag, other.type_tag) in self.adders:
                return self.cross_apply(other, self.adders)
        def __mul__(self, other):
            if self.type_tag == other.type_tag:
                return self.mul(other)
            elif (self.type_tag, other.type_tag) in self.multipliers:
                return self.cross_apply(other, self.multipliers)
        def cross_apply(self, other, cross_fns):
            cross_fn = cross_fns[(self.type_tag, other.type_tag)]
            return cross_fn(self, other)
        adders = {("com", "rat"): add_complex_and_rational,
                    ("rat", "com"): add_rational_and_complex}
        multipliers = {("com", "rat"): mul_complex_and_rational,
                        ("rat", "com"): mul_rational_and_complex}
    这里我们还设置了addersmultipliers两个字典,用于存储Numbers支持的不同类相加的组合;同时设置cross_apply函数,用于找到对应的字典内方法并调用。
    具体使用例:
    >>> ComplexRI(1.5, 0) + Rational(3, 2)
    ComplexRI(3, 0)
    >>> Rational(-1, 2) * ComplexMA(4, pi/2)
    ComplexMA(2, 1.5 * pi)
  • 这里增加新的组合计算方式同样很方便:只需要从Numbers继承新的类,并在addersmultipliers中加入新的组合计算方法即可(也可以在类中定义字典)

类型强制转换(type coercion)

  • 上面的类型分派方法更适用于两个完全不相关的类之间的跨类操作。而如果两个类之间存在一定的转化关系(如上面的复数与有理数),那么就可以通过强制类型转换变为一个类,再调用内部方法实现。比如将有理数转化为复数:
    >>> def rational_to_complex(r):
        return ComplexRI(r.numer/r.denom, 0)
    然后我们再对Numbers的内容进行修改,保留type_tag属性,然后设置coercions字典与coercecoerce_to两个函数:
    >>> class Number:
        def __add__(self, other):
            x, y = self.coerce(other)
            return x.add(y)
        def __mul__(self, other):
            x, y = self.coerce(other)
            return x.mul(y)
        def coerce(self, other): # 将有理数强制转换为复数
            if self.type_tag == other.type_tag:
                return self, other
            elif (self.type_tag, other.type_tag) in self.coercions:
                return (self.coerce_to(other.type_tag), other)
            elif (other.type_tag, self.type_tag) in self.coercions:
                return (self, other.coerce_to(self.type_tag))
        def coerce_to(self, other_tag): # 调用字典内方法计算
            coercion_fn = self.coercions[(self.type_tag, other_tag)]
            return coercion_fn(self)
        coercions = {('rat', 'com'): rational_to_complex}
    注意coercions字典内的key是有序的,因为复数无法强制转换为有理数。
  • 这种方法相比类型派发更加简洁,当然也对类型关系的要求就更高(即类型转换只与数据本身有关,而与类型操作无关)。当然,也有将两种类型都强制转换为一种通用类型、或者一种类型通过链式转换到另一种类型(可以减少强制转换的函数总量)
  • 不过强制类型转换也有其代价——在转换过程中可能会损失信息。

在python的早期版本中,对象都会自带__coerce__方法用于强制类型转换。但python 3移除了这一方法,转而让运算符根据需要对数据进行类型转换。