先说明一下,特殊方法的存在是为了被 Python 解释器调用的,我们自己不需要调用它。也就是说没有 my_object.__len__() 这种写法,而是应该使用 len(my_object)。一般来说,通过内置函数(len, iter, str 等等)来使用特殊方法是最好的选择,这些内置函数不仅会调用特殊方法,通常还会提供特殊的好处。而且对于内置类来说,它的速度更快。

实现向量类

通过特殊方法,我们可以自定义对象的 '+' 操作,以后的章节会对此详细介绍,这里只是展示一下特殊方法的使用。

我们想要实现一个简单的支持加法,取绝对值,标量乘法的二维向量类

In [21]:
from math import hypot

class Vector:
    def __init__(self, x = 0, y = 0):
        self.x = x
        self.y = y
    
    def __repr__(self):
        # %r 获取对象各个属性标准字符串表现形式,这是个好习惯,它说明了一个关键点,Vector(1,2) 和 vector('1','2') 是不一样的
        # 后者会在定义的时候报错,因为对象的构造只接收数值,不接受字符串
        return "Vector(%r, %r)" % (self.x, self.y)
    
    def __abs__(self):
        return hypot(self.x, self.y) #返回欧几里德范数 sqrt(x*x + y*y)
    
    def __bool__(self):
        return bool(abs(self))
    
    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return Vector(x, y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

__repr__

它能把一个对象用字符串的形式表现出来。

In [16]:
test = Vector()
test #如果注释 __repr__() 方法, 显示 <__main__.Vector at 0x7f587c4c1320>
Out[16]:
Vector(0, 0)

__repr__() 能把一个对象用字符串的方式表达出来,这就是字符串表示形式。它的返回应该尽量精确的与表达出创建它的对象, 与 __str__() 比较, __str__() 是由 str() 函数调用,并可以让 print() 函数使用。并且它返回的字符串应该对终端用户更友好。 如果你只想实现这两个方法的其中一个,好的选择是 __repr__(),因为一个对象没有 __str__() 函数,python 又要调用它的时候,解释器会使用 __repr__() 来代替。

In [19]:
str(test)
Out[19]:
'Vector(0, 0)'

算数运算符

+* 分别调用的是 __add____mul__ 注意这里我们没有修改 self.x, self.y,而是返回了一个新的实例,这是中缀表达式的基本原则,不改变操作对象

In [20]:
v1 = Vector(2, 4)
v2 = Vector(2, 1)
v1 + v2
Out[20]:
Vector(4, 5)
In [22]:
v1 * 3
Out[22]:
Vector(6, 12)

注意现在我们只能将一个类乘数字,而不能用数字乘类,到后面的章节我们会实现这种乘法的交换性,使用 __rmul__() 解决这个问题。

自定义类型的布尔值

在 if, while 等陈述式运算式,或者 and, or, not 等运算,为了判断值是 true 或 false,Python 会调用 bool() 函数,其实背后调用的是 __bool__(),它永远只返回 True 或 False。

在默认情况下,使用者自定义的实例都是 True,除非这个类对于 __bool__() 或者 __len__() 有自己的实现,如果你没有 __bool__,会尝试调用 __len__,如果 __len__() 等于 0,返回 False,否则返回 True

我们的 __bool__ 逻辑很简单,看向量长度是否为 0。

如果想要 Vector.__bool__ 更高效,可以采用这种实现

In [1]:
#def __bool__(self):
#    return bool(self.x or self.y)

虽然不是那么易读,但是省去了平方操作。把返回值显式转成 bool 格式是为了符合 __bool__() 对返回值的规定,因为 or 运算符可能返回 x 或 y 本身的值,如果 x 为真,返回的是 x 值,y 为真,返回的是 y 的值。

为什么 len 不是一种方法

Raymond Hettinger 说: "practicality beats purity"(实用胜于纯粹)。如果 x 是一个内置类型的实例, len(x) 将会很快,背后的 CPython 会直接从 C 结构体中取长度,完全不用调用任何方法。因为 len() 是 CPython 内建方法,跑起来非常快,len() 是一种常见操作,所以要保证效率。

换句话说 len() 不是一个普通的方法,是为了让 Python 自带数据结构走后门,abs 也是同理。但是也亏了它们是特殊方法,我们也可以把 len 用于自定义数据类型。这种处理方式在保持内置类型效率和语言的一致性找到了一个平衡点。

如果把 abs 和 len 看成一元运算符可能更容易被接受,它们虽然看起来像函数,但其实不是,有一门叫 ABC 的语言是 Python 的直系祖先,它内置了一个 # 运算符,当你写出 #s 时候,它的作用和 len 一样,如果写成 x#s 这样的中缀运算符的话,它的作用是计算 s 中 x 出现的次数。python 中对应的写法是 s.count(x)。注意这里 s 是一个序列类型