流畅的 Python 第二版(GPT 重译)(八)(3)https://developer.aliyun.com/article/1484691
示例 16-10. vector_v6.py:向 vector_v5.py 添加了运算符+
方法(示例 12-16)
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
注意,__add__
现在捕获TypeError
并返回NotImplemented
。
警告
如果中缀运算符方法引发异常,则会中止运算符分派算法。在TypeError
的特定情况下,通常最好捕获它并返回 NotImplemented
。这允许解释器尝试调用反向运算符方法,如果它们是不同类型的,则可能正确处理交换操作数的计算。
到目前为止,我们已经通过编写__add__
和__radd__
安全地重载了+
运算符。现在我们将处理另一个中缀运算符:*
。
为标量乘法重载*
Vector([1, 2, 3]) * x
是什么意思?如果x
是一个数字,那将是一个标量乘积,结果将是一个每个分量都乘以x
的新Vector
——也被称为逐元素乘法:
>>> v1 = Vector([1, 2, 3]) >>> v1 * 10 Vector([10.0, 20.0, 30.0]) >>> 11 * v1 Vector([11.0, 22.0, 33.0])
注意
涉及Vector
操作数的另一种产品类型将是两个向量的点积,或者矩阵乘法,如果你将一个向量视为 1×N 矩阵,另一个向量视为 N×1 矩阵。我们将在我们的Vector
类中实现该运算符,详见“使用@作为中缀运算符”。
再次回到我们的标量乘积,我们从可能起作用的最简单的__mul__
和__rmul__
方法开始:
# inside the Vector class def __mul__(self, scalar): return Vector(n * scalar for n in self) def __rmul__(self, scalar): return self * scalar
这些方法确实有效,除非提供了不兼容的操作数。scalar
参数必须是一个数字,当乘以一个float
时产生另一个float
(因为我们的Vector
类在内部使用float
数组)。因此,一个complex
数是不行的,但标量可以是一个int
、一个bool
(因为bool
是int
的子类),甚至是一个fractions.Fraction
实例。在示例 16-11 中,__mul__
方法没有对scalar
进行显式类型检查,而是将其转换为float
,如果失败则返回NotImplemented
。这是鸭子类型的一个明显例子。
示例 16-11. vector_v7.py:添加*
方法
class Vector: typecode = 'd' def __init__(self, components): self._components = array(self.typecode, components) # many methods omitted in book listing, see vector_v7.py # in https://github.com/fluentpython/example-code-2e def __mul__(self, scalar): try: factor = float(scalar) except TypeError: # ① return NotImplemented # ② return Vector(n * factor for n in self) def __rmul__(self, scalar): return self * scalar # ③
①
如果scalar
无法转换为float
…
②
…我们不知道如何处理它,所以我们返回NotImplemented
,让 Python 尝试在scalar
操作数上执行__rmul__
。
③
在这个例子中,__rmul__
通过执行self * scalar
来正常工作,委托给__mul__
方法。
通过示例 16-11,我们可以将Vectors
乘以通常和不太常见的数值类型的标量值:
>>> v1 = Vector([1.0, 2.0, 3.0]) >>> 14 * v1 Vector([14.0, 28.0, 42.0]) >>> v1 * True Vector([1.0, 2.0, 3.0]) >>> from fractions import Fraction >>> v1 * Fraction(1, 3) Vector([0.3333333333333333, 0.6666666666666666, 1.0])
现在我们可以将Vector
乘以标量,让我们看看如何实现Vector
乘以Vector
的乘积。
注意
在Fluent Python的第一版中,我在示例 16-11 中使用了鹅类型:我用isinstance(scalar, numbers.Real)
检查了__mul__
的scalar
参数。现在我避免使用numbers
ABCs,因为它们不受 PEP 484 支持,而且在运行时使用无法静态检查的类型对我来说似乎不是一个好主意。
或者,我可以针对我们在“运行时可检查的静态协议”中看到的typing.SupportsFloat
协议进行检查。在那个示例中,我选择了鸭子类型,因为我认为精通 Python 的人应该对这种编码模式感到舒适。
另一方面,在示例 16-12 中的__matmul__
是鹅类型的一个很好的例子,这是第二版中新增的。
使用@作为中缀运算符
@
符号众所周知是函数装饰器的前缀,但自 2015 年以来,它也可以用作中缀运算符。多年来,在 NumPy 中,点积被写为numpy.dot(a, b)
。函数调用符号使得从数学符号到 Python 的长公式更难以转换,因此数值计算社区游说支持PEP 465—用于矩阵乘法的专用中缀运算符,这在 Python 3.5 中实现。今天,你可以写a @ b
来计算两个 NumPy 数组的点积。
@
运算符由特殊方法__matmul__
、__rmatmul__
和__imatmul__
支持,命名为“矩阵乘法”。这些方法目前在标准库中没有被使用,但自 Python 3.5 以来,解释器已经认可它们,因此 NumPy 团队——以及我们其他人——可以在用户定义的类型中支持@
运算符。解析器也已更改以处理新运算符(在 Python 3.4 中,a @ b
是语法错误)。
这些简单的测试展示了@
应该如何与Vector
实例一起工作:
>>> va = Vector([1, 2, 3]) >>> vz = Vector([5, 6, 7]) >>> va @ vz == 38.0 # 1*5 + 2*6 + 3*7 True >>> [10, 20, 30] @ vz 380.0 >>> va @ 3 Traceback (most recent call last): ... TypeError: unsupported operand type(s) for @: 'Vector' and 'int'
示例 16-12 展示了相关特殊方法的代码。
示例 16-12. vector_v7.py:操作符@
方法
class Vector: # many methods omitted in book listing def __matmul__(self, other): if (isinstance(other, abc.Sized) and # ① isinstance(other, abc.Iterable)): if len(self) == len(other): # ② return sum(a * b for a, b in zip(self, other)) # ③ else: raise ValueError('@ requires vectors of equal length.') else: return NotImplemented def __rmatmul__(self, other): return self @ other
①
两个操作数必须实现__len__
和__iter__
…
②
…并且具有相同的长度以允许…
③
…sum
、zip
和生成器表达式的一个美妙应用。
Python 3.10 中的新 zip() 特性
zip
内置函数自 Python 3.10 起接受一个strict
关键字参数。当strict=True
时,当可迭代对象的长度不同时,函数会引发ValueError
。默认值为False
。这种新的严格行为符合 Python 的快速失败哲学。在示例 16-12 中,我会用try/except ValueError
替换内部的if
,并在zip
调用中添加strict=True
。
示例 16-12 是实践中鹅类型的一个很好的例子。如果我们将other
操作数与Vector
进行测试,我们将剥夺用户使用列表或数组作为@
操作数的灵活性。只要一个操作数是Vector
,我们的@
实现就支持其他操作数是abc.Sized
和abc.Iterable
的实例。这两个 ABC 都实现了__subclasshook__
,因此任何提供__len__
和__iter__
的对象都满足我们的测试——无需实际子类化这些 ABC,甚至无需向它们注册,如“使用 ABC 进行结构化类型检查”中所解释的那样。特别是,我们的Vector
类既不是abc.Sized
的子类,也不是abc.Iterable
的子类,但它通过了对这些 ABC 的isinstance
检查,因为它具有必要的方法。
在深入讨论“富比较运算符”的特殊类别之前,让我们回顾一下 Python 支持的算术运算符。
算术运算符总结
通过实现+
、*
和@
,我们看到了编写中缀运算符的最常见模式。我们描述的技术适用于表 16-1 中列出的所有运算符(就地运算符将在“增强赋值运算符”中介绍)。
表 16-1. 中缀运算符方法名称(就地运算符用于增强赋值;比较运算符在表 16-2 中)
运算符 | 正向 | 反向 | 就地 | 描述 |
+ |
__add__ |
__radd__ |
__iadd__ |
加法或连接 |
- |
__sub__ |
__rsub__ |
__isub__ |
减法 |
* |
__mul__ |
__rmul__ |
__imul__ |
乘法或重复 |
/ |
__truediv__ |
__rtruediv__ |
__itruediv__ |
真除法 |
// |
__floordiv__ |
__rfloordiv__ |
__ifloordiv__ |
地板除法 |
% |
__mod__ |
__rmod__ |
__imod__ |
取模 |
divmod() |
__divmod__ |
__rdivmod__ |
__idivmod__ |
返回地板除法商和模数的元组 |
** , pow() |
__pow__ |
__rpow__ |
__ipow__ |
指数运算^(a) |
@ |
__matmul__ |
__rmatmul__ |
__imatmul__ |
矩阵乘法 |
& |
__and__ |
__rand__ |
__iand__ |
位与 |
| | __or__ |
__ror__ |
__ior__ |
位或 |
^ |
__xor__ |
__rxor__ |
__ixor__ |
位异或 |
<< |
__lshift__ |
__rlshift__ |
__ilshift__ |
位左移 |
>> |
__rshift__ |
__rrshift__ |
__irshift__ |
位右移 |
^(a) pow 接受一个可选的第三个参数,modulo :pow(a, b, modulo) ,在直接调用时也由特殊方法支持(例如,a.__pow__(b, modulo) )。 |
富比较运算符使用不同的规则。
富比较运算符
Python 解释器对富比较运算符==
、!=
、>
、<
、>=
和<=
的处理与我们刚才看到的类似,但在两个重要方面有所不同:
- 在前向和反向运算符调用中使用相同的方法集。规则总结在表 16-2 中。例如,在
==
的情况下,前向和反向调用都调用__eq__
,只是交换参数;前向调用__gt__
后跟着反向调用__lt__
,参数交换。 - 在
==
和!=
的情况下,如果缺少反向方法,或者返回NotImplemented
,Python 会比较对象 ID 而不是引发TypeError
。
表 16-2. 富比较运算符:当初始方法调用返回NotImplemented
时调用反向方法
组 | 中缀运算符 | 前向方法调用 | 反向方法调用 | 回退 |
相等性 | a == b |
a.__eq__(b) |
b.__eq__(a) |
返回id(a) == id(b) |
a != b |
a.__ne__(b) |
b.__ne__(a) |
返回not (a == b) |
|
排序 | a > b |
a.__gt__(b) |
b.__lt__(a) |
引发TypeError |
a < b |
a.__lt__(b) |
b.__gt__(a) |
引发TypeError |
|
a >= b |
a.__ge__(b) |
b.__le__(a) |
引发TypeError |
|
a <= b |
a.__le__(b) |
b.__ge__(a) |
引发TypeError |
鉴于这些规则,让我们审查并改进Vector.__eq__
方法的行为,该方法在vector_v5.py中编码如下(示例 12-16):
class Vector: # many lines omitted def __eq__(self, other): return (len(self) == len(other) and all(a == b for a, b in zip(self, other)))
该方法产生了示例 16-13 中的结果。
示例 16-13. 将Vector
与Vector
、Vector2d
和tuple
进行比较
>>> 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 # ③ True
①
具有相等数值组件的两个Vector
实例比较相等。
②
如果它们的组件相等,Vector
和Vector2d
也相等。
③
Vector
也被视为等于包含相同数值的tuple
或任何可迭代对象。
示例 16-13 中的结果可能不理想。我们真的希望Vector
被视为等于包含相同数字的tuple
吗?我对此没有硬性规定;这取决于应用上下文。《Python 之禅》说:
面对模棱两可的情况,拒绝猜测的诱惑。
在评估操作数时过于宽松可能导致令人惊讶的结果,程序员讨厌惊喜。
借鉴于 Python 本身,我们可以看到[1,2] == (1, 2)
是False
。因此,让我们保守一点并进行一些类型检查。如果第二个操作数是Vector
实例(或Vector
子类的实例),那么使用与当前__eq__
相同的逻辑。否则,返回NotImplemented
并让 Python 处理。参见示例 16-14。
示例 16-14. vector_v8.py:改进了Vector
类中的__eq__
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 # ②
①
如果other
操作数是Vector
的实例(或Vector
子类的实例),则像以前一样执行比较。
②
否则,返回NotImplemented
。
如果您使用来自示例 16-14 的新Vector.__eq__
运行示例 16-13 中的测试,现在得到的结果如示例 16-15 所示。
示例 16-15. 与示例 16-13 相同的比较:最后结果改变
>>> 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
①
与预期一样,与之前相同的结果。
②
与之前相同的结果,但为什么?解释即将到来。
③
不同的结果;这就是我们想要的。但是为什么会起作用?继续阅读…
在 示例 16-15 中的三个结果中,第一个不是新闻,但最后两个是由 示例 16-14 中的 __eq__
返回 NotImplemented
导致的。以下是在一个 Vector
和一个 Vector2d
的示例中发生的情况,vc == v2d
,逐步进行:
- 要评估
vc == v2d
,Python 调用Vector.__eq__(vc, v2d)
。 Vector.__eq__(vc, v2d)
验证v2d
不是Vector
并返回NotImplemented
。- Python 得到
NotImplemented
的结果,因此尝试Vector2d.__eq__(v2d, vc)
。 Vector2d.__eq__(v2d, vc)
将两个操作数转换为元组并进行比较:结果为True
(Vector2d.__eq__
的代码在 示例 11-11 中)。
至于比较 va == t3
,在 示例 16-15 中的 Vector
和 tuple
之间,实际步骤如下:
- 要评估
va == t3
,Python 调用Vector.__eq__(va, t3)
。 Vector.__eq__(va, t3)
验证t3
不是Vector
并返回NotImplemented
。- Python 得到
NotImplemented
的结果,因此尝试tuple.__eq__(t3, va)
。 tuple.__eq__(t3, va)
不知道什么是Vector
,所以返回NotImplemented
。- 在
==
的特殊情况下,如果反向调用返回NotImplemented
,Python 将比较对象 ID 作为最后的手段。
对于 !=
我们不需要为 __ne__
实现,因为从 object
继承的 __ne__
的后备行为适合我们:当 __eq__
被定义且不返回 NotImplemented
时,__ne__
返回该结果的否定。
换句话说,给定我们在 示例 16-15 中使用的相同对象,!=
的结果是一致的:
>>> va != vb False >>> vc != v2d False >>> va != (1, 2, 3) True
从 object
继承的 __ne__
的工作方式如下代码所示——只是原始代码是用 C 编写的:⁶
def __ne__(self, other): eq_result = self == other if eq_result is NotImplemented: return NotImplemented else: return not eq_result
在介绍了中缀运算符重载的基本知识之后,让我们转向另一类运算符:增强赋值运算符。
增强赋值运算符
我们的 Vector
类已经支持增强赋值运算符 +=
和 *=
。这是因为增强赋值对于不可变接收者通过创建新实例并重新绑定左侧变量来工作。
示例 16-16 展示了它们的运行方式。
示例 16-16. 使用 +=
和 *=
与 Vector
实例
>>> v1 = Vector([1, 2, 3]) >>> v1_alias = v1 # ① >>> id(v1) # ② 4302860128 >>> v1 += Vector([4, 5, 6]) # ③ >>> v1 # ④ Vector([5.0, 7.0, 9.0]) >>> id(v1) # ⑤ 4302859904 >>> v1_alias # ⑥ Vector([1.0, 2.0, 3.0]) >>> v1 *= 11 # ⑦ >>> v1 # ⑧ Vector([55.0, 77.0, 99.0]) >>> id(v1) 4302858336
①
创建一个别名,以便稍后检查 Vector([1, 2, 3])
对象。
②
记住绑定到 v1
的初始 Vector
的 ID。
③
执行增强加法。
④
预期的结果…
⑤
…但是创建了一个新的 Vector
。
⑥
检查 v1_alias
以确认原始的 Vector
没有被改变。
⑦
执行增强乘法。
⑧
再次,预期的结果,但是创建了一个新的 Vector
。
如果一个类没有实现 Table 16-1 中列出的原地操作符,增强赋值运算符将作为语法糖:a += b
将被完全解释为 a = a + b
。这是对于不可变类型的预期行为,如果你有 __add__
,那么 +=
将可以工作而无需额外的代码。
然而,如果你实现了一个原地操作符方法,比如 __iadd__
,那么该方法将被调用来计算 a += b
的结果。正如其名称所示,这些操作符预期会就地更改左操作数,并且不会像结果那样创建一个新对象。
警告
不可变类型如我们的 Vector
类不应该实现原地特殊方法。这是相当明显的,但无论如何值得声明。
为了展示就地运算符的代码,我们将扩展BingoCage
类,从示例 13-9 实现__add__
和__iadd__
。
我们将子类称为AddableBingoCage
。示例 16-17 是我们想要+
运算符的行为。
示例 16-17。+
运算符创建一个新的AddableBingoCage
实例
>>> vowels = 'AEIOU' >>> globe = AddableBingoCage(vowels) # ① >>> globe.inspect() ('A', 'E', 'I', 'O', 'U') >>> globe.pick() in vowels # ② True >>> len(globe.inspect()) # ③ 4 >>> globe2 = AddableBingoCage('XYZ') # ④ >>> globe3 = globe + globe2 >>> len(globe3.inspect()) # ⑤ 7 >>> void = globe + [10, 20] # ⑥ Traceback (most recent call last): ... TypeError: unsupported operand type(s) for +: 'AddableBingoCage' and 'list'
①
创建一个具有五个项目(每个vowels
)的globe
实例。
②
弹出其中一个项目,并验证它是否是vowels
之一。
③
确认globe
只剩下四个项目。
④
创建第二个实例,有三个项目。
⑤
通过将前两个实例相加创建第三个实例。这个实例有七个项目。
⑥
尝试将AddableBingoCage
添加到list
中会导致TypeError
。当我们的__add__
方法返回NotImplemented
时,Python 解释器会产生该错误消息。
因为AddableBingoCage
是可变的,示例 16-18 展示了当我们实现__iadd__
时它将如何工作。
示例 16-18。现有的AddableBingoCage
可以使用+=
加载(继续自示例 16-17)
>>> globe_orig = globe # ① >>> len(globe.inspect()) # ② 4 >>> globe += globe2 # ③ >>> len(globe.inspect()) 7 >>> globe += ['M', 'N'] # ④ >>> len(globe.inspect()) 9 >>> globe is globe_orig # ⑤ True >>> globe += 1 # ⑥ Traceback (most recent call last): ... TypeError: right operand in += must be 'Tombola' or an iterable
①
创建一个别名,以便稍后检查对象的标识。
②
这里的globe
有四个项目。
③
一个AddableBingoCage
实例可以接收来自同一类的另一个实例的项目。
④
+=
的右操作数也可以是任何可迭代对象。
⑤
在整个示例中,globe
一直指的是与globe_orig
相同的对象。
⑥
尝试将不可迭代的内容添加到AddableBingoCage
中会失败,并显示适当的错误消息。
注意+=
运算符相对于第二个操作数更加宽松。对于+
,我们希望两个操作数的类型相同(在这种情况下为AddableBingoCage
),因为如果我们接受不同类型,可能会导致对结果类型的混淆。对于+=
,情况更加清晰:左侧对象在原地更新,因此对结果的类型没有疑问。
提示
通过观察list
内置类型的工作方式,我验证了+
和+=
的对比行为。编写my_list + x
,你只能将一个list
连接到另一个list
,但如果你写my_list += x
,你可以使用右侧的任何可迭代对象x
扩展左侧的list
。这就是list.extend()
方法的工作方式:它接受任何可迭代的参数。
现在我们清楚了AddableBingoCage
的期望行为,我们可以查看其在示例 16-19 中的实现。回想一下,BingoCage
,来自示例 13-9,是Tombola
ABC 的具体子类,来自示例 13-7。
示例 16-19。bingoaddable.py:AddableBingoCage
扩展BingoCage
以支持+
和+=
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: # ⑤ msg = ('right operand in += must be ' "'Tombola' or an iterable") raise TypeError(msg) self.load(other_iterable) # ⑥ return self # ⑦
①
AddableBingoCage
扩展BingoCage
。
②
我们的__add__
只能与Tombola
的实例作为第二个操作数一起使用。
③
在__iadd__
中,从other
中检索项目,如果它是Tombola
的实例。
④
否则,尝试从other
中获取一个迭代器。⁷
⑤
如果失败,引发一个解释用户应该做什么的异常。 在可能的情况下,错误消息应明确指导用户解决方案。
⑥
如果我们走到这一步,我们可以将 other_iterable
加载到 self
中。
⑦
非常重要:可变对象的增强赋值特殊方法必须返回 self
。 这是用户的期望。
我们可以通过对比在示例 16-19 中产生结果的 __add__
和 __iadd__
中的 return
语句来总结就地运算符的整个概念:
__add__
通过调用构造函数 AddableBingoCage
来生成结果以构建一个新实例。
__iadd__
通过修改后返回 self
生成结果。
结束这个示例时,对示例 16-19 的最后观察:按设计,AddableBingoCage
中没有编写 __radd__
,因为没有必要。 前向方法 __add__
仅处理相同类型的右操作数,因此如果 Python 尝试计算 a + b
,其中 a
是 AddableBingoCage
而 b
不是,则返回 NotImplemented
—也许 b
的类可以使其工作。 但是如果表达式是 b + a
而 b
不是 AddableBingoCage
,并且返回 NotImplemented
,那么最好让 Python 放弃并引发 TypeError
,因为我们无法处理 b
。
提示
一般来说,如果一个前向中缀运算符方法(例如 __mul__
)设计为仅与与 self
相同类型的操作数一起使用,那么实现相应的反向方法(例如 __rmul__
)是没有用的,因为根据定义,只有在处理不同类型的操作数时才会调用它。
我们的 Python 运算符重载探索到此结束。
章节总结
我们从回顾 Python 对运算符重载施加的一些限制开始:不能在内置类型本身中重新定义运算符,重载仅限于现有运算符,有一些运算符被排除在外(is
、and
、or
、not
)。
我们从一元运算符入手,实现了 __neg__
和 __pos__
。 接下来是中缀运算符,从 +
开始,由 __add__
方法支持。 我们看到一元和中缀运算符应通过创建新对象来生成结果,并且永远不应更改其操作数。 为了支持与其他类型的操作,我们返回 NotImplemented
特殊值—而不是异常—允许解释器通过交换操作数并调用该运算符的反向特殊方法(例如 __radd__
)再次尝试。 Python 用于处理中缀运算符的算法在图 16-1 中总结。
混合操作数类型需要检测我们无法处理的操作数。 在本章中,我们以两种方式实现了这一点:在鸭子类型方式中,我们只是继续尝试操作,如果发生 TypeError
异常,则捕获它;稍后,在 __mul__
和 __matmul__
中,我们通过显式的 isinstance
测试来实现。 这些方法各有利弊:鸭子类型更灵活,但显式类型检查更可预测。
一般来说,库应该利用鸭子类型——打开对象的大门,无论它们的类型如何,只要它们支持必要的操作即可。然而,Python 的运算符分发算法可能在与鸭子类型结合时产生误导性的错误消息或意外的结果。因此,在编写用于运算符重载的特殊方法时,使用isinstance
调用 ABCs 进行类型检查的纪律通常是有用的。这就是亚历克斯·马特利所称的鹅类型技术,我们在“鹅类型”中看到了。鹅类型是灵活性和安全性之间的一个很好的折衷方案,因为现有或未来的用户定义类型可以声明为 ABC 的实际或虚拟子类。此外,如果一个 ABC 实现了__subclasshook__
,那么对象通过提供所需的方法可以通过该 ABC 的isinstance
检查—不需要子类化或注册。
我们接下来讨论的话题是丰富的比较运算符。我们用__eq__
实现了==
,并发现 Python 在object
基类中提供了一个方便的!=
实现,即__ne__
。Python 评估这些运算符的方式与>
, <
, >=
, 和 <=
略有不同,对于选择反向方法有特殊逻辑,并且对于==
和!=
有后备处理,因为 Python 比较对象 ID 作为最后的手段,从不生成错误。
在最后一节中,我们专注于增强赋值运算符。我们看到 Python 默认将它们处理为普通运算符后跟赋值的组合,即:a += b
被完全解释为a = a + b
。这总是创建一个新对象,因此适用于可变或不可变类型。对于可变对象,我们可以实现就地特殊方法,比如__iadd__
用于+=
,并改变左操作数的值。为了展示这一点,我们放下了不可变的Vector
类,开始实现一个BingoCage
子类,支持+=
用于向随机池添加项目,类似于list
内置支持+=
作为list.extend()
方法的快捷方式。在这个过程中,我们讨论了+
相对于接受的类型更为严格的问题。对于序列类型,+
通常要求两个操作数是相同类型,而+=
通常接受任何可迭代对象作为右操作数。
进一步阅读
Guido van Rossum 在“为什么运算符有用”中写了一篇很好的运算符重载辩护。Trey Hunner 在博客“Python 中的元组排序和深度比较”中辩称,Python 中的丰富比较运算符比程序员从其他语言转换过来时可能意识到的更灵活和强大。
运算符重载是 Python 编程中一个常见的地方,其中isinstance
测试很常见。围绕这些测试的最佳实践是鹅类型,详见“鹅类型”。如果你跳过了这部分,请确保阅读一下。
运算符特殊方法的主要参考是 Python 文档中的“数据模型”章节。另一个相关阅读是Python 标准库中numbers
模块的“9.1.2.2. 实现算术运算”。
一个聪明的运算符重载例子出现在 Python 3.4 中添加的pathlib
包中。它的Path
类重载了/
运算符,用于从字符串构建文件系统路径,如文档中所示的示例:
>>> p = Path('/etc') >>> q = p / 'init.d' / 'reboot' >>> q PosixPath('/etc/init.d/reboot')
另一个非算术运算符重载的例子是Scapy库,用于“发送、嗅探、解剖和伪造网络数据包”。在 Scapy 中,/
运算符通过堆叠来自不同网络层的字段来构建数据包。详见“堆叠层”。
如果你即将实现比较运算符,请研究functools.total_ordering
。这是一个类装饰器,可以自动生成定义了至少一些富比较运算符的类中的所有富比较运算符的方法。请参考functools 模块文档。
如果你对动态类型语言中的运算符方法分派感兴趣,两篇开创性的文章是 Dan Ingalls(原 Smalltalk 团队成员)的“处理多态的简单技术”,以及 Kurt J. Hebel 和 Ralph Johnson(Johnson 因为是原始《设计模式》书籍的作者之一而出名)的“Smalltalk-80 中的算术和双重分派”。这两篇论文深入探讨了动态类型语言(如 Smalltalk、Python 和 Ruby)中多态的强大之处。Python 不使用这些文章中描述的双重分派来处理运算符。Python 算法使用前向和后向运算符对于用户定义的类来说更容易支持,但需要解释器进行特殊处理。相比之下,经典的双重分派是一种通用技术,你可以在 Python 或任何面向对象的语言中使用,超越了中缀运算符的特定上下文,事实上,Ingalls、Hebel 和 Johnson 使用非常不同的例子来描述它。
文章“C 语言家族:与丹尼斯·里奇、比雅尼·斯特劳斯特鲁普和詹姆斯·高斯林的访谈”,我引用了本章前言中的摘录,发表于Java Report,2000 年 7 月,第 5 卷第 7 期,以及C++ Report,2000 年 7 月/8 月,第 12 卷第 7 期,还有本章“讲台”中使用的另外两个片段。如果你对编程语言设计感兴趣,请务必阅读该访谈。
¹ 来源:“C 语言家族:与丹尼斯·里奇、比雅尼·斯特劳斯特鲁普和詹姆斯·高斯林的访谈”。
² Python 标准库中剩余的 ABC 对于鹅类型和静态类型仍然有价值。numbers
ABC 的问题在“数字 ABC 和数值协议”中有解释。
³ 请参考https://en.wikipedia.org/wiki/Bitwise_operation#NOT解释按位非操作。
⁴ Python 文档同时使用这两个术语。“数据模型”章节使用“reflected”,但numbers
模块文档中的“9.1.2.2. 实现算术运算”提到“forward”和“reverse”方法,我认为这个术语更好,因为“forward”和“reversed”清楚地命名了每个方向,而“reflected”没有明显的对应词。
⁵ 请参考“讲台”讨论该问题。
⁶ object.__eq__
和object.__ne__
的逻辑在 CPython 源代码的Objects/typeobject.c中的object_richcompare
函数中。
⁷ iter
内置函数将在下一章中介绍。在这里,我可以使用tuple(other)
,它也可以工作,但会建立一个新的tuple
,而所有.load(…)
方法需要的只是对其参数进行迭代。