运算符重载的作用是让用户定义的对象使用中缀运算符(如 + 和 |) 或一元运算符(如 - 和 ~)。说的宽泛一些,在 Python 中,函数调用(()
),属性访问(.
)和元素访问/切片([]
) 也是运算符,不过本章只讨论一元运算符和中缀运算符。
在接下来的几节,我们将讨论:
- Python 如何处理中缀运算符中不同类型的操作数
- 使用鸭子类型或显式类型检查处理不同类型的操作数
- 中缀运算符如何表明自己无法处理操作数
- 众多比较运算符(如 ==, >, <=, 等等) 的特殊行为
- 增量赋值运算符(如 +=)的默认处理方式和重载方式
运算符重载基础
- 不能重载内置类型的运算符
- 不能新建运算符,只能重载现有的
- 某些运算符不能重载—
is
,and
,or
和not
(不过位运算符&
,|
, 和~
可以)
一元运算符
下面是三个一元运算符和对应的特殊方法
-
-(
__neg__
)一元取负算术运算符。x = -2 -> -x = 2
-
+(
__pos__
)一元取正算术运算符。通常 x = +x, 但也有一些例外。
-
~(
__invert__
)对整数按位取反,定义为 ~x = -(x+1)。如果 x 是 2,那么 ~x = -3
支持一元运算符很简单,只需实现相应的特殊方法。这些特殊方法只有一个参数,self.然后,使用符合所在类的逻辑实现。不过,要遵守运算符的一个基本规则:始终返回一个新对象。也就是说,不能修改 self, 要创建并返回合适类型的心实例。
重载向量加法运算符+
重载向量加法运算符很简单,即只要在类中实现 __add__
方法即可,但Python为中缀运算符特殊方法提供了特殊的分派机制。对表达式 a+b
来说,解释器会执行以下几步操作。
- 如果
a
有__add__
方法,而且返回值不是NotImplemented
, 调用a.__add__(b)
, 然后返回结果。 - 如果
a
没有__add__
方法,或者调用__add__
方法返回NotImplemented
,检查b
有没有__radd__
方法,如果有,而且没有返回NotImplemented
, 调用b.__radd__(a)
,然后返回结果。 - 如果
b
没有__radd__
方法,或者调用__radd__
方法返回NotImplemented
, 抛出TypeError
, 并在错误消息中指明操作数类型不支持。
- tips: 别把
NotImplemented
和NotImplementedError
搞混了。前者是特殊的单例值,如果中缀运算符特殊方法不能处理给定的操作数,那么要把它返回给解释器。而NotImplementedError
是一种异常,抽象类中的占位方法把它抛出,提醒子类必须覆盖。
众多比较运算符
Python 解释器对众多比较运算符(==, !=, >, <, >=, <=)的处理与前文类似,不过在两个方面有重大区别。
- 正向和反向调用使用的是同一系列方法。例如,对 == 来说,正向和反向调用都是
__eq__
方法,只能把参数对调了;而正向的__gt__
方法调用的是反向的__lt__
方法,并把参数对调。 - 对
==
和!=
来说,如果反向调用失败,Python 会比较对象的 ID,而不抛出 TypeError。 - Python3 的新行为:Python2 之后的比较运算符后备机制都变了。对于
__ne__
,现在 Python3 返回结果是对__eq__
结果的取反。对于排序比较运算符,Python3 抛出TypeError
, 并把错误消息设为unorderable types: int() < tuple()
。在 Python2 中,这些比较的结果很怪异,会考虑对象的类型和ID,而且无规律可循。
增量赋值运算符
- 增量赋值不会修改不可变目标,而是新建实例,然后重新绑定。
- 如果一个类没有实现就地运算符,增量赋值运算符只是语法糖:
a += b
的作用与a = a + b
完全一样。对不可变类型来说,这是预期的行为,而且,如果定义了__add__
方法的话,不用编写额外的代码,+=
就能使用。 - 如果实现了就地运算符方法,例如
__iadd__
, 计算a += b
的结果时会调用就地运算符方法。这种运算符的名称表明,它们会就地修改左操作符,而不会创建新对象作为结果。