流畅的 Python 第二版(GPT 重译)(八)(4)

简介: 流畅的 Python 第二版(GPT 重译)(八)

流畅的 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(因为boolint的子类),甚至是一个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__

…并且具有相同的长度以允许…

sumzip和生成器表达式的一个美妙应用。

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.Sizedabc.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 接受一个可选的第三个参数,modulopow(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. 将VectorVectorVector2dtuple进行比较
>>> 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实例比较相等。

如果它们的组件相等,VectorVector2d也相等。

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,逐步进行:

  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) 将两个操作数转换为元组并进行比较:结果为 TrueVector2d.__eq__ 的代码在 示例 11-11 中)。

至于比较 va == t3,在 示例 16-15 中的 Vectortuple 之间,实际步骤如下:

  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 作为最后的手段。

对于 != 我们不需要为 __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,是TombolaABC 的具体子类,来自示例 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,其中 aAddableBingoCageb 不是,则返回 NotImplemented—也许 b 的类可以使其工作。 但是如果表达式是 b + ab 不是 AddableBingoCage,并且返回 NotImplemented,那么最好让 Python 放弃并引发 TypeError,因为我们无法处理 b

提示

一般来说,如果一个前向中缀运算符方法(例如 __mul__)设计为仅与与 self 相同类型的操作数一起使用,那么实现相应的反向方法(例如 __rmul__)是没有用的,因为根据定义,只有在处理不同类型的操作数时才会调用它。

我们的 Python 运算符重载探索到此结束。

章节总结

我们从回顾 Python 对运算符重载施加的一些限制开始:不能在内置类型本身中重新定义运算符,重载仅限于现有运算符,有一些运算符被排除在外(isandornot)。

我们从一元运算符入手,实现了 __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(…)方法需要的只是对其参数进行迭代。

相关文章
|
13天前
|
存储 自然语言处理 安全
流畅的 Python 第二版(GPT 重译)(十)(1)
流畅的 Python 第二版(GPT 重译)(十)
60 0
|
JavaScript 物联网 Linux
流畅的 Python 第二版(GPT 重译)(二)(3)
流畅的 Python 第二版(GPT 重译)(二)
61 4
|
13天前
|
缓存 算法 Java
流畅的 Python 第二版(GPT 重译)(三)(4)
流畅的 Python 第二版(GPT 重译)(三)
43 4
|
存储 JSON 数据格式
流畅的 Python 第二版(GPT 重译)(八)(2)
流畅的 Python 第二版(GPT 重译)(八)
66 0
|
存储 算法 安全
流畅的 Python 第二版(GPT 重译)(二)(2)
流畅的 Python 第二版(GPT 重译)(二)
108 4
|
13天前
|
消息中间件 缓存 应用服务中间件
流畅的 Python 第二版(GPT 重译)(十)(4)
流畅的 Python 第二版(GPT 重译)(十)
38 0
|
13天前
|
存储 API uml
流畅的 Python 第二版(GPT 重译)(七)(1)
流畅的 Python 第二版(GPT 重译)(七)
85 1
|
13天前
|
存储 Java 测试技术
流畅的 Python 第二版(GPT 重译)(四)(1)
流畅的 Python 第二版(GPT 重译)(四)
48 1
|
安全 测试技术 程序员
流畅的 Python 第二版(GPT 重译)(三)(2)
流畅的 Python 第二版(GPT 重译)(三)
31 11
|
13天前
|
安全 Java 程序员
流畅的 Python 第二版(GPT 重译)(六)(3)
流畅的 Python 第二版(GPT 重译)(六)
9 1

热门文章

最新文章