运算符重载
本章将讨论:
- Python 如何处理中缀运算符中不同类型的操作数
- 使用鸭子类型或显式类型检查处理不同类型的操作数
- 中缀运算符如何表明自己无法处理操作数
- 众多比较运算符(如 ==、 >、 <=,等等)的特殊行为
- 增量赋值运算符(如 +=)的默认处理方式和重载方式
1. 运算符重载基础
Python 限制:
- 不能重载内置类型的运算符
- 不能新建运算符,只能重载现有的
- 某些运算符不能重载——is、 and、 or 和 not(不过位运算符 &、 | 和 ~ 可以)
运算符的基本规则:
- 就地运算符(增量赋值运算符)必须返回 self
- 除就地运算符外,其他运算符始终返回一个新对象,即要创建并返回合适类型的新实例
2. 一元运算符
2.1 一元运算符简介
- 文档: Python 语言参考手册,“6.6. Unary arithmetic and bitwise operations"一节
- 链接: https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations
|运算符|方法|说明|示例| |: —|: —|: —|: —| |-|__neg__|一元取负算术运算符|如果 x 是 -2,那么 -x == 2| |+|__pos__|一元取正算术运算符|通常 x == +x,但也有一些例外| |~|__invert__|对整数按位取反,定义为 ~x == -(x+1)|如果 x 是 2,那么 ~x == -3| |abs()|__abs__|取绝对值|…|
2.2 x 和 +x 何时不相等
decimal.Decimal 类:
- 场景: 在不同精度的上下文中计算 +x,那么 x != +x
- 原因: 虽然每个 +one_third 表达式都会使用 one_third 创建一个新 Decimal 实例,但是 会使用当前算术运算上下文的精度
|
|
collections.Counter:
- 加法运算符: Counter 相加时,负值和零值计数会从结果中剔除
- 一元运算符: 等同于加上一个空Counter,因此产生一个新的 Counter 且仅保留大于零的计数器
- 文档: https://docs.python.org/3/library/collections.html#collections.Counter
|
|
3. 中缀运算符
|运算符|正向方法|反向方法|就地方法|说明| |: —|: —|: —|: —|: —| |+|__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__|取幂 *| |@|__matmul__|__rmatmul__|__imatmul__|矩阵乘法 #| |&|__and__|__rand__|__iand__|位与| ||__or__|__ror__|__ior__|位或| |^|__xor__|__rxor__|__ixor__|位异或| |«|__lshift__|__rlshift__|__ilshift__|按位左移| |»|__rshift__|__rrshift__|__irshift__|按位右移|
3.1 中缀运算符分派机制
** a + b 执行步骤**
- 如果 a 有 __add__ 方法,且返回值不是 NotImplemented,调用 a.__add__(b)返回结果
- 如果 a 没有 __add__ 方法,或者调用 __add__ 方法返回 NotImplemented,检查 b 有没有 __radd__ 方法,如果有,而且没有返回 NotImplemented,调用 b.__radd__(a),然后返回结果
- 如果 b 没有 __radd__ 方法,或者调用 __radd__ 方法返回 NotImplemented,抛出 TypeError, 并在错误消息中指明操作数类型不支持
- 说明:
- 右向(right)特殊方法(又称反向方法)提供了一种后备机制
- 如果中缀运算符方法抛出异常,就会终止运算符分派机制
- 一般来说,如果中缀运算符的正向方法只处理与 self 属于同一类型的操作数, 那就无需实现对应的反向方法,因为按照定义,反向方法是为了处理类型不同的操作数
NotImplemented 和 NotImplementedError
- NotImplemented:
- 特殊的单例值
- 如果中缀运算符特殊方法不能处理给定的操作数,那么要把它返回(return)给解释器
- NotImplementedError:
- 是一种异常
- 抽象类中的占位方法把它抛出(raise),提醒子类必须覆盖
3.2 加法运算符 + 示例
|
|
示例分析:
- __radd__ 直接委托 __add__,前提是运算符可交换
- 如果由于类型不兼容而导致运算符特殊方法无法返回有效的结果,那么应该返回 NotImplemented, 而不是抛出 TypeError,这样另一个操作数所属的类型还有机会执行运算,即Python 会尝试调用反向方法
- 如果反向方法返回 NotImplemented,那么 Python 会抛出 TypeError,并返回一个标准的错 误消息,例如” unsupported operand type(s) for +: Vector and str"
- 为了遵守鸭子类型精神,不能测试 other 操作数的类型,或者它的元素的类型。应该要捕获异常, 然后返回 NotImplemented
3.2 乘法运算符 * 示例
|
|
示例分析:
- 白鹅类型的实际运用: 显式检查抽象类型 numbers.Real 抽象基类
- numbers.Real 抽象基类涵盖了我们所需的全部数值类型,而且还支持以后声明为 numbers.Real 抽象基类的真实子类或虚拟子类的数值类型
- 鸭子类型更灵活,但显式检查更能预知结果,通过检查抽象基类在灵活性和安全性之间做了很好的折中
- decimal.Decimal 没有把自己注册为 numbers.Real 的虚拟子类。 因此, Vector 类不会处理 decimal.Decimal 数字
3.3 特殊运算符说明
pow(x, y[, z])
- 两个参数 == x**y
- 三个参数 == (x**y) % z,但 pow() 函数更高效
@ 运算符
- 版本: Python35 引入
- 作用: 矩阵乘法,点积
4. 比较运算符
4.1 比较运算符分派机制
分派机制对比:
- 相似: 与中缀运算符分配机制类似,正向方法返回NotImplemented,调用反向方法
- 区别:
- 正向和反向调用使用的是同一系列方法
- __eq__ 方法,只是把参数对调了;
- 正向的 __gt__ 方法调用的是反向的 __lt__ 方法,并把参数对调
- 对 == 和 != 来说,如果反向调用失败, Python 会比较对象的 ID,而不抛出 TypeError
|分组|中缀运算符|正向方法调用|反向方法调用|后备机制| |: —|: —|: —|: —|: —| |相等性|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|
版本差异:
- __ne__
- Python 3 的后备机制是对 __eq__ 结果的取反,因此对于 != 运算符,无需重载
- Python 2 不是如此
- 比较运算符
- Python 3 抛出 TypeError,并把错误消息设为 ‘unorderable types: int() < tuple()
- Python 2 中,这些比较的结果很怪异,会考虑对象的类型和 ID,而且无规律可循
5. 就地运算符
分派机制:
- 如果类没有实现就地运算符,增量赋值运算符只是语法糖: a += b 的作用与 a = a + b 一样, 对不可变类型来说,这是预期的行为,而且,如果定义了__add__ 方法,不用编写额外的代码, += 就能使用
- 如果类实现了就地运算符方法,会就地修改左操作数,而不会创建新对象作为结果, 不可变类型,一定不能实现就地特殊方法
+ 与 +=:
- + 必需返回类的新实例,+= 必须返回 self,即实例本身
- 与 + 相比, += 运算符对第二个操作数更宽容,因为
- + 运算符的两个操作数必须是相同类型,如若不然,结果的类型可能让人摸不着头脑
- 而 += 的情况更明确,因为就地修改左操作数,所以结果的类型是确定的
|
|
延伸阅读
Python:
运算符特殊方法
numbers 模块
- 9.1.2.2. Implementing the arithmeticoperations 一节
- https://docs.python.org/3/library/numbers.html#implementing-the-arithmeticoperations
functools.total_ordering
- 作用: 能为只定义了几个比较运算符的类自动生成全部比较运算符
- 文档: https://docs.python.org/3/library/functools.html#functools.total_ordering
blog:
What’s New inPython 3.0
运算符方法分派机制
- 这两篇论文深入分析了动态类型语言(如 Smalltalk、 Python 和 Ruby)的多态
- A Simple Technique for Handling Multiple Polymorphism https://wiki.illinois.edu//wiki/download/attachments/273416327/ingalls.pdf
- Arithmetic and Double Dispatching in Smalltalk-80 https://wiki.illinois.edu//wiki/download/attachments/273416327/double-dispatch.pdf
- Python 没有使用这两篇论文中所述的双重分配处理运算符, Python 使用的正向运算符和反 向运算符更便于用户定义的类支持双重分派,但是这种方式需要解释器做些特殊处理
实用工具
书籍:
《 Python Cookbook(第 3 版)中文版》
- “9.20 通过函数注解来实现方法重载"秘笈使用一些高级元编程(涉及元类)通过函数注解实现了基于类型的分派
《 Python Cookbook(第 2 版)中文版》
- 2.13 节,展示了如何重载 « 运算符,在 Python 中模仿 C++ 的 iostream 句法
附注
运算符重载
- 对极为重视性能和安全的低级系统语言而言,不支持运算符重载这无疑是正确的决定
- 但是,重载的运算符,如果使用得当,的确能让代码更易于阅读和编写。对现代的高级语言来说,这是个好功能