Fluent Python 第十三章 正确使用运算符重载

Fluent Python 第十三章读书报告

Chapter 13. Operator Overloading: Doing It Right

第十三章: 正确使用运算符重载

运算符重载的作用是让用户定义的对象使用中缀运算符或一元运算符。宽泛一些来讲,Python中的函数调用(()),属性访问(.),和元素访问/切片([])也是运算符,不过本章只讨论一元运算符和中缀运算符。

接下来我们会讨论这几个问题:

  • Python中如何处理运算符中不同类型的操作数
  • 使用压制类型或者显式类型检查处理不同类型的操作数
  • 中缀运算符如何表明自己无法处理操作数
  • 众多比较运算符(==, > ,<=)等的特殊行为
  • 增量赋值运算符的默认处理方式和重载方式

13.1 运算符重载基础

Python 在运算符重载方面进行了一些限制,做好了灵活性、可用性和安全性方面的平衡。

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

13.2 一元运算符

Python语言参考手册中,列出了3个一元运算符:

  • - 取负运算符,对应特殊方法 __neg__
  • + 取正运算符,对应特殊方法 __pos__
  • ~ 取反运算符,对应特殊方法 __invert__

Python 中还有一个较为特殊的一元运算符abs(), 取绝对值操作符,对应的是特殊方法 __abs__

支持一元运算符很简单,只需要实现相应的特殊方法。这些特殊方法只有一个参数self。需要遵循的基本规则是始终返回一个新对象,也就是说不能修改self, 而是要创建并返回合适类型的新实例。

一般来说, -,+ 返回与self同一类型的实例。abs一般返回一个标量。对于~来说,很难说什么结果是合理的,例如ORM中SQL WHERE语句取反就应该返回反集。

13.3 为Vector重载向量加法运算符+

之前的章节中我们实现过向量Vector 现在为它实现运算符重载

序列应当支持+运算符(用于拼接), 以及*运算符(用于重复复制)

然而我们要做的是,为向量实现向量加法和向量乘法运算(点乘)

我们想象中的向量加法应该实现下面两点:

  • 对于两个维度相同的向量, 分量分别相加
  • 对于两个维度不同的向量,分量少的尾部补零向量再相加

基于以上两点,可以初步写出相应的特殊方法:

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

需要注意的点: 实现医院运算符和中缀运算符的特殊方法一定不能修改操作数,这些操作符理应返回新对象,只有增量赋值表达式可能修改第一个操作数。

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

  • 如果a有__add__方法, 而且返回值不是NotImplemented, 调用a.__add__(b)
  • 如果没有__add__方法, 或者__add__方法返回NotImplemented, 检查b有没有__radd__, 如果有且返回不是NotImplemented,返回b.__radd__(a)
  • 如果b没有__radd__或者__radd__返回NotImplemented, 抛出TypeError, traceback 中会指明操作数不支持。

具体的情况如下图所示:
Figure-13-1

NotImplemented和NotImplementedError是不同的, 前者是特殊的单例值,在运算符无法处理操作数时返回(return)给解释器,后者是一种异常,抽象类抛出(raise)这个异常提醒子类必须覆盖。

NotImplemented 与Error的不同在于,返回NotImplemented时, 另一个操作数还有机会执行反向的运算方法。这是Python种运算符的一种分派机制。

13.4 重载标量乘法运算符*

向量的标量积(scalar product) 有两种:

  • 向量与标量相乘, 结果是与原向量方向相同, 模是原向量模的标量倍的向量
  • 向量与向量相乘,结果是各个分量的积求和的标量

NumPy等库目前的做法是,不重载这两种意义的*, 向量与向量相乘的情况用 numpy.dot()处理

类似加号的重载, 我们为Vector 写好了__mul__和__rmul__方法。

1
2
3
4
def __mul__(self, scalar):
return Vector(n * scalar for n in self)
def __rmul__(self, scalar):
return self * scalar

Python 3.5 以后的版本提供了@运算符, 计算两个向量相乘的标量积。特殊方法为__matmul__

13.5 比较运算符

Python 解释器对众多比较运算符的处理与前文类似,不过有两点比较明显的区别:

  • 正向和反向调用使用的是同一系列方法。例如,对 == 来说,正向和反向调用都是 __eq__ 方法,只是把参数对调了;而正向的 __gt__ 方法调用的是反向的 __lt__ 方法,并把参数对调。
  • 对 == 和 != 来说,如果反向调用失败,Python 会比较对象的 ID,而不抛出TypeError。

Python3 中 __ne__的结果是对 __eq__ 方法取反的结果, 一些不合适的比较会返回TypeError而不比较对象ID

13.6 增量赋值运算符

增量赋值不会修改不可变目标,而是新建实例然后重新绑定。
需要指出的是, 不可变类型一定不能实现就地特殊方法,即修改实例本身的方法

如果一个类没有实现就地运算符对应的特殊方法,增量赋值运算符只是语法糖:a += b 的作用与 a = a + b 完全一样。对不可变类型来说,这是预期的行为,而且,如果定义了__add__ 方法的话,不用编写额外的代码,+= 就能使用。
然而,如果实现了就地运算符方法,例如 __iadd__,计算 a += b 的结果时会调用就地运算符方法。这种运算符的名称表明,它们会就地修改左操作数,而不会创建新对象作为结果。

13.7 小结

本章主要介绍了Python中对运算符重载的做法, 限制和特性。