十三、正确重载运算符
运算符重载的作用:让用户定义的对象使用中缀运算符或一元运算符。
Python 施加了一些限制,做好了灵活性、可用性和安全性方面的平衡:
1 不能重载内置类型的运算符
2 不能新建运算符,只能重载现有的
3 某些运算符不能重载—— is , and , or , not (位运算符 & , | , ~ 可以)
一元运算符
-
: __neg__
一元取负算术运算符。
+
: __pos__
一元取正算术运算符。
~
: __inveret__
对整数按位取反, ~x == -(x+1)
abs
: __abs__
取绝对值
一元运算符,只有一个参数 :self ,返回:一个新的同类型对象
def __abs__(self):
return math.sqrt(sum(x * x for x in self))
def __neg__(self):
return Vector(-x for x in self)
def __pos__(self):
return Vector(self)
x 和 +x 何时不相等
虽然每个 +one_third 表达式都会使用 one_third 的值创建一个新 Decimal 实例,但是会使用当前算术运算上下文的精度。精度不同,导致不相等。
In [8]: import decimal
In [9]: ctx = decimal.getcontext() # 获取当前全局运算符的上下文引用
In [10]: ctx.prec = 40 # 设置算术运算符上下文精度为:40
In [11]: a = decimal.Decimal('1')/decimal.Decimal('3') # 求 1/3
In [12]: a
Out[12]: Decimal('0.3333333333333333333333333333333333333333')
In [13]: a == +a # 此时为 True
Out[13]: True
In [14]: ctx.prec = 28 # 调整精度
In [15]: a == +a # 为 False
Out[15]: False
In [16]: +a
Out[16]: Decimal('0.3333333333333333333333333333')
In [17]: a
Out[17]: Decimal('0.3333333333333333333333333333333333333333')
collections.Counter 会清除计数器中的 负数数量和零数量的值,导致不相等。
In [36]: from collections import Counter
In [37]: c = Counter('abcde')
In [38]: c
Out[38]: Counter({'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
In [39]: c['a'] = -1
In [40]: c
Out[40]: Counter({'a': -1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
In [41]: c['b'] = 0
In [42]: c
Out[42]: Counter({'a': -1, 'b': 0, 'c': 1, 'd': 1, 'e': 1})
In [43]: +c
Out[43]: Counter({'c': 1, 'd': 1, 'e': 1})
In [44]: c == +c
Out[44]: False
重载向量加法运算符 +
实现效果:
>>> v1 = Vector([3, 4, 5, 6])
>>> v3 = Vector([1, 2])
>>> v1 + v3
Vector([4.0, 6.0, 5.0, 6.0])
代码:
def __add__(self, other):
pairs = itertools.zip_longest(self, other, fillvalue=0) # 根据长度长的,填充0,生成器
return Vector(a + b for a, b in pairs) # 生成器,返回 Vector 新实例,不影响 self 或 other
实现一元运算符和中缀运算符的特殊方法一定不能修改操作数。使用这些运算符的表达式期待结果是新对象。只有增量赋值表达式可能会修改第一个操作数(self)。
为了支持涉及不同类型的运算,Python 为中缀运算符特殊方法提供了特殊的分派机制。对表达式 a + b 来说,解释器会执行以下几步操作
(1) 如果 a 有 __add__
方法,而且返回值不是 NotImplemented,调用 a.__add__(b)
,然后返回结果。
(2) 如果 a 没有 __add__
方法,或者调用 __add__
方法返回 NotImplemented,检查 b有没有 __radd__
方法,如果有,而且没有返回 NotImplemented,调用 b.__radd__(a)
,然后返回结果。
(3) 如果 b 没有 __radd__
方法,或者调用 __radd__
方法返回 NotImplemented,抛出TypeError,并在错误消息中指明操作数类型不支持。
__radd__
是 __add__
的“反射”(reflected)版本或“反向”(reversed)版本。我喜欢把它叫作“反向”特殊方法。本书的三位技术审校,Alex、Anna 和 Leo 告诉我,他们喜欢称之为“右向”(right)特殊方法,因为他们在右操作数上调用。
反射特殊方法,反向特殊方法,右向特殊方法:__radd__
我们要实现 Vector.__radd__
方法。这是一种后备机制,如果左操作数没有实现 __add__
方法,或者实现了,但是返回 NotImplemented 表明它不知道如何处理右操作数,那么 Python 会调用 __radd__
方法。
NotImplemented
特殊的单例值,如果中缀运算符特殊方法不能处理给定的操作数,那么要把它返回(return)给解释器。
NotImplementedError
一种异常,抽象类中的占位方法把它抛出(raise),提醒子类必须覆盖。
实现 __radd__
def __add__(self, other): # 实现正向特殊方法
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a + b for a, b in pairs)
def __radd__(self, other): # 实现反向特殊方法,直接委托 __add__。任何可交换的运算符都能这么做。
return self + other
处理数字和向量时,+ 可以交换,但是拼接序列时不行。会抛出没有太多作用的异常。
如果类型不兼容,就返回 NotImplemented,反向方法也返回 NotImplemented,就抛出标准错误消息:
“unsupported operand type(s) for +: Vector and str”
为了遵守鸭子类型精神,我们不能测试 other 操作数的类型,或者它的元素的类型。我们要捕获异常,然后返回 NotImplemented。
实现:
def __add__(self, other):
try:
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a + b for a, b in pairs)
except TypeError:
return NotImplemented
def __radd__(self, other):
return self + other
重载标量乘法运算符 *
白鹅类型的实际运用——显式检查抽象类型。
def __mul__(self, scalar):
if isinstance(scalar, numbers.Real):
return Vector(n * scalar for n in self)
else:
return NotImplemented
def __rmul__(self, scalar):
return self * scalar
比较运算符
Python 解释器对众多比较运算符(==、!=、>、<、>=、<=)的处理与前文类似,不过在两个方面有重大区别。
1 正向和反向调用使用的是同一系列方法。这方面的规则如表 13-2 所示。例如,对 == 来说,正向和反向调用都是 __eq__
方法,只是把参数对调了;而正向的 __gt__
方法调用的是反向的 __lt__
方法,并把参数对调。
2 对 == 和 != 来说,如果反向调用失败,Python 会比较对象的 ID,而不抛出 TypeError。
class Vector:
def __eq__(self, other):
return (len(self) == len(other) and all(a == b for a, b in zip(self, other)))
上述方法,列表也可以进行比较。
“Python 之禅”说道:
如果存在多种可能,不要猜测。
对操作数过度宽容可能导致令人惊讶的结果,而程序员讨厌惊喜。
从 Python 自身来找线索,我们发现 [1,2] == (1, 2) 的结果是 False。因此,我们要保守一点,做些类型检查。如果第二个操作数是 Vector 实例(或者 Vector 子类的实例),那么就使用 __eq__
方法的当前逻辑。否则,返回 NotImplemented,让 Python 处理。
改进后:
def __eq__(self, other):
if isinstance(other, Vector):
return (len(self) == len(other) and all(a == b for a, b in zip(self, other)))
else:
return NotImplemented
>>> va = Vector([1.0, 2.0, 3.0])
>>> vb = Vector(range(1, 4))
>>> va == vb
True
>>> vc = Vector([1, 2])
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> vc == v2d
True
>>> t3 = (1, 2, 3)
>>> va == t3
False
(1) 为了计算 vc == v2d,Python 调用 Vector.__eq__(vc, v2d)。
(2) 经 Vector.__eq__(vc, v2d) 确认,v2d 不是 Vector 实例,因此返回NotImplemented。
(3) Python 得到 NotImplemented 结果,尝试调用 Vector2d.__eq__(v2d, vc)。
(4) Vector2d.__eq__(v2d, vc) 把两个操作数都变成元组,然后比较,结果是True
(1) 为了计算 va == t3,Python 调用 Vector.__eq__(va, t3)。
(2) 经 Vector.__eq__(va, t3) 确认,t3 不是 Vector 实例,因此返回NotImplemented。
(3) Python 得到 NotImplemented 结果,尝试调用 tuple.__eq__(t3, va)。
(4) tuple.__eq__(t3, va) 不知道 Vector 是什么,因此返回 NotImplemented。
(5) 对 == 来说,如果反向调用返回 NotImplemented,Python 会比较对象的 ID,作最后一搏。
增量赋值运算符
如果一个类没有实现表 13-1 列出的就地运算符,增量赋值运算符只是语法糖:a += b 的作用与 a = a + b 完全一样。对不可变类型来说,这是预期的行为,而且,如果定义了 __add__
方法的话,不用编写额外的代码,+= 就能使用。
结果与预期相符,创建了新的 Vector 实例。
然而,如果实现了就地运算符方法,例如 __iadd__
,计算 a += b 的结果时会调用就地运算符方法。这种运算符的名称表明,它们会就地修改左操作数,而不会创建新对象作为结果。
不可变类型,一定不能实现就地特殊方法。这是明显的事实,不过还是值得提出来。
注意,与 + 相比,+= 运算符对第二个操作数更宽容。+ 运算符的两个操作数必须是相同类型(这里是 AddableBingoCage),如若不然,结果的类型可能让人摸不着头脑。而 += 的情况更明确,因为就地修改左操作数,所以结果的类型是确定的。
通过观察内置 list 类型的工作方式,我确定了要对 + 和 += 的行为做什么限制。 my_list + x 只能用于把两个列表加到一起,而 my_list += x 可以使用右边可迭代对象 x 中的元素扩展左边的列表。list.extend() 的行为也是这样的,它的参数可以是任何可迭代对象。
import itertools
from tombola import Tombola
from bingo import BingoCage
class AddableBingoCage(BingoCage):
def __add__(self, other):
if isinstance(other, Tombola):
return AddableBingoCage(self.inspect() + other.inspect())
else:
return NotImplemented
def __iadd__(self, other):
if isinstance(other, Tombola):
other_iterable = other.inspect()
else:
try:
other_iterable = iter(other)
except TypeError:
raise TypeError(msg.format(self_cls))
self.load(other_iterable)
return self # 重要提醒:增量赋值特殊方法必须返回 self
__add__
调用构造方法构建一个新实例,作为结果返回。
__iadd__
把修改后的 self 作为结果返回。
一般来说,如果中缀运算符的正向方法(如 __mul__
)只处理与 self 属于同一类型的操作数,那就无需实现对应的反向方法(如 __rmul__
),因为按照定义,反向方法是为了处理类型不同的操作数。
Python 对运算符重载施加的一些限制:禁止重载内置类型的运算符,而且限于重载现有的运算符,不过有几个例外(is、and、or、not)。
如果操作数的类型不同,我们要检测出不能处理的操作数。本章使用两种方式处理这个问题:一种是鸭子类型,直接尝试执行运算,如果有问题,捕获 TypeError 异常;另一种是显式使用 isinstance 测试。
这两种方式各有优缺点:鸭子类型更灵活,但是显式检查更能预知结果。如果选择使用 isinstance,要小心,不能测试具体类,而要测试 numbers.Real 抽象基类,例如 isinstance(scalar,numbers.Real)。这在灵活性和安全性之间做了很好的折中,因为当前或未来由用户定义的类型可以声明为抽象基类的真实子类或虚拟子类,
Python 特别处理 == 和 != 的后备机制:从不抛出错误,因为 Python 会比较对象的 ID,作最后一搏。
在 Python 编程中,运算符重载经常使用 isinstance 做测试。一般来说,库应该利用动态类型(提高灵活性),避免显式测试类型,而是直接尝试操作,然后处理异常,这样只要对象支持所需的操作即可,而不必一定是某种类型。但是,Python 抽象基类允许一种更为严格的鸭子类型,Alex Martelli 称之为“白鹅类型”,编写重载运算符的代码时经常能用到。
元组:用到的时候才会使用,而不是定义的时候。