《流畅的Python》读书笔记15(第十三章:正确重载运算符)

160 阅读4分钟

运算符重载的作用是让用户定义的对象使用中缀运算符(如+和|)或一元运算符(如-和~)。

13.1 运算符重载基础

Python对运算符重载做了一些限制:

  • 不能重载内置类型的运算符
  • 不能新建运算符,只能重载现有的
  • 某些运算符不能重载——is、and、or 和 not

13.2 一元运算符

- (__neg__) 取负运算符。如果x 是-2,那么-x == 2

+ (__pos__) 取正运算符

~ (__invert__) 对整数按位取反

支持一元运算符很简单,只需实现相应的特殊方法。这些特殊方法只有一个参数,self。然后,使用符合所在类的逻辑实现。不过,哟啊遵守运算符的一个基本规则:始终返回一个新对象。

示例13-1 vector_v6.py:新增一元运算符 - 和 +

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)

13.3 重载向量加法运算符+

两个欧几里得向量加在一起得到的是一个新向量,它的各个分量是两个向量中相应的分量之后。

示例13-4 Vector.__add__方法

def __add__(self, other):
    pairs = itertools.zip_longest(self, other, fillvalue=0)
    return Vector(a+b for a, b in pairs)
from vector_v6 import Vector
v1 = Vector([3,4,5])
v2 = Vector([6,7,8])
v1 + v2
Vector([9.0, 11.0, 13.0])
v1 + v2 == Vector([3+6, 4+7, 5+8])
True

示例13-5 第一版Vector.__add__方法也支持Vector之外的对象

from vector2d_v3 import Vector2d
v1 = Vector([3,4,5])
v1 + (10, 20, 30)
Vector([13.0, 24.0, 35.0])
v2d = Vector2d(1, 2)
v1 + v2d
Vector([4.0, 6.0, 5.0])

示例13-6 如果左操作数是Vector之外的对象,第一版Vector.__add__方法无法处理

v1 = Vector([3, 4, 5])
(10, 20, 30) + v1
Traceback (most recent call last):
  ...
TypeError: can only concatenate tuple (not "Vector") to tuple
from vector2d_v3 import Vector2d
v2d = Vector2d(1, 2)
v2d + v1
Traceback (most recent call last):
  ...
TypeError: unsupported operand type(s) for +: 'Vector2d' and 'Vector'

为了支持涉及不同类型的运算,Python为中缀运算符特殊方法提供了特殊的分派机制。对表达式a+b来说,解释器会执行以下几步

image.png __radd__是__add__的“反射”版本或“反向”版本。

示例13-7 Vector.__add__和__radd__方法

def __add__(self, other):
    pairs = itertools.zip_longest(self, other, fillvalue=0)
    return Vector(a+b for a, b in pairs)

def __radd__(self, other):
    return self + other

示例13-8 Vector.__add__方法的操作数要是可迭代对象

v1 = Vector([3, 4, 5])
v1 + 1
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  ...
    pairs = itertools.zip_longest(self, other, fillvalue=0)
TypeError: zip_longest argument #2 must support iteration

如果由于类型不兼容而导致运算符特殊方法无法返回有效的结果,那么应该返回NotImplemented,而不是抛出TypeError。返回NotImplemented时,另一个操作数所属的类型还有机会执行运算,即Python会尝试调用反向方法。

示例13-10: 修改__add__方法

def __add__(self, other):
    try:
        pairs = itertools.zip_longest(self, other, fillvalue=0)
        return Vector(a+b for a, b in pairs)
    except TypeError:
        return NotImplemented

13.4 重载标量乘法运算符*

先实现最简单的__mul__和__rmul__

def __mul__(self, scalar):
    return Vector(n * scalar for n in self)

def __rmul__(self, scalar):
    return self * scalar

这两个方法确实可用,但是提供不兼容的操作数时会出问题。

示例13-11 vector_v7.py: 增加*运算符方法

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
from vector_v7 import Vector
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])

13.5 众多比较运算符

Python解释器对众多比较运算符(==、 !=、 <、 >、 >=、 <=)的处理与前文类似,不过在两个方面有重大区别。

  • 正向和反向调用使用同一系列方法。
  • 对== 和!=来说,如果反向调用失败,Python会比较对象的ID,而不抛出TypeError。

image.png

示例13-12

va = Vector([1.0, 2.0, 3.0])
vb = Vector(range(1, 4))
va == vb
True
vc = Vector([1, 2])
t3 = (1,2,3)
va == t3
True

最后一个结果可能不是很理想,我们改进下:

示例13-13 改进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

13.6 增量赋值运算符

Vector类已经支持增量赋值运算符+= 和 *=

示例13-5 增量运算符不会修改不可变目标,而是新建实例

v1 = Vector([1, 2, 3])
v1_alias = v1
id(v1)
140727697662584
v1 += Vector([4, 5, 6])
v1
Vector([5.0, 7.0, 9.0])
id(v1)
140727697662864
v1_alias
Vector([1.0, 2.0, 3.0])
v1 *= 11
v1
Vector([55.0, 77.0, 99.0])
id(v1)
140727697662472

如果一个类没有实现__iadd__就地加法运算符,增量赋值运算符只是语法糖:a += b的作用与a = a + b完全一样。

然而如果实现了就地运算符方法,例如__iadd__,计算 a + b的结果时会调用就地运算符方法。这种运算符的名称表明,它们会就地修改左操作数,而不会创建新对象作为结果。

示例13-18 bingoaddable.py: 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:
            self_cls = type(self).__name__
            msg = "right operand in += must be {!r} or an iterable"
            raise TypeError(msg.format(self_cls))
    self.load(other_iterable)
    return self