《流畅的Python》笔记。
本篇是“面向对象惯用方法”的第六篇,也是最后一篇。本篇将讨论Python中的运算符重载。
1. 前言
Python中的运算符重载和C++中的运算符重载并不一样,C++中同一运算符可以有多个重载函数,Python中的运算符重载其实是实现运算符的同名特殊方法。
本篇只讨论一元运算符和中缀运算符,内容如下:
- Python如何处理中缀运算符中不同类型的操作数;
- 使用鸭子类型或白鹅类型处理不同类型的操作数;
- 中缀运算符如何表明自己无法处理操作数;
- 众多比较运算符的特殊行为;
- 增量运算符的默认处理方式和重载方式。
不过,需要说明的是,并不是所有的运算符都能重载:
- 不能重载内置类型的运算符;
- 不能新建运算符,只能重载现有的;
-
is
,and
,or
和not
不能重载。
本文中的示例延用
《Python学习之路29》
中的多维向量
Vector
。
2. 一元运算符
本节主要介绍4个一元运算符,它们分别是:
-
-
(__neg__
):一元取负运算符,如x = 2
,则-x == 2
; -
+
(__pos__
):一元取正运算符,通常是x == +x
,但也有特例; -
~
(__invert__
):对整数按位取反,定义为~x == -(x + 1)
; -
abs()
函数:Python语言参考手册把它也列为了一元运算符,它对应的就是之前多次用到的__abs__
。
在实现过程中需要遵循
这些
运算符的一个基本规则:
始终返回一个新对象
!也就是说不能修改
self
,要创建并返回合适类型的实例。以下补充两个
Vector
类的运算符重载:
def __neg__(self):
return Vector(-x for x in self)
def __pos__(self):
return Vector(self)
复制代码
x
和
+x
何时不等
?以下是两个例子:
-
如果
decimal.Decimal
所在上下文的精度不同,则有可能不等,如下:>>> import decimal >>> ctx = decimal.getcontext() >>> ctx.prec = 40 >>> one_third = decimal.Decimal("1") / decimal.Decimal("3") >>> one_third Decimal('0.3333333333333333333333333333333333333333') >>> one_third == +one_third True >>> ctx.prec = 28 # 这是默认精度 >>> one_third == +one_third False >>> +one_third Decimal('0.3333333333333333333333333333') 复制代码
-
collections.Counter
在相加时,负值和零值计数会从结果中剔除,而一元运算符+
对它来说等同于加上一个空Counter
,如下:>>> ct = Counter("abracadabra") >>> ct["r"] = -3 >>> ct["d"] = 0 >>> ct Counter({'a': 5, 'b': 2, 'c': 1, 'd': 0, 'r': -3}) >>> +ct # 与ct不等 Counter({'a': 5, 'b': 2, 'c': 1}) 复制代码
3. 重载向量加法运算符+
目前版本的
Vector
不支持向量相加,因为没有重载
+
运算符。我们的要求如下:
-
它能实现两个
Vector
相加,并且两个长度不等的Vector
也能相加,短的那个用0.0
填充; -
能与任何可迭代对象相加,但当这个可迭代对象中的元素不能与浮点数做加法运算时,则抛出
NotImplemented
异常;
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,而不是raise
return NotImplemented
def __radd__(self, other): # 实现反向相加
return self + other
# 在控制台中运行的示例,省略了import语句
>>> v1 = Vector([1, 2, 3])
>>> v1 + Vector([2, 3, 4]) # 可以和同类型的相加
Vector([3.0, 5.0, 7.0])
>>> v1 + (1, 2, 3) # 和其他可迭代对象也能相加
Vector([2.0, 4.0, 6.0])
>>> v1 + (1, 2) # 长度不同也能相加
Vector([2.0, 4.0, 3.0])
>>> v1 + Vector2d(1, 2) # 由于我们之前实现的Vector2d也是可迭代对象,所以也能和Vector相加
Vector([2.0, 4.0, 3.0])
>>> (1, 2, 3) + v1 # <1> 反向也能相加,见解释
Vector([2.0, 4.0, 6.0])
复制代码
解释 :
-
像
__radd__
,__rsub__
这种前面带r
的方法一般被称作“反向”运算方法或“右向”运算方法,如果没有实现这种方法,上述代码<1>
处的语句就会抛出TypeError
; -
对于表达式
a + b
来说,解释器会执行如下几步:-
如果
a
有__add__
方法,调用a.__add__(b)
; -
如果
a.__add__(b)
返回NotImplemented
,或者a
没有__add__
方法,则检查b
有没有__radd__
方法,如果有,则调用b.__radd__(a)
; -
如果
b.__radd__(a)
返回NotImplemented
,或者b
没有__radd__
方法,则抛出TypeError
,并在错误消息中指明 操作数类型不支持 。
其他有反向运算方法的运算符在调用时也是上面这个逻辑。
-
如果
-
__radd__
等反向运算的实现通常就如上述代码这么简单暴力:直接委托给正向运算。 -
在实现
__add__
时,我们并没有去判断other
的类型或者它的元素的类型,而是捕获TypeError
异常。这是在给other
调用反向运算方法的一个机会。如果调用成功,other
就能被当做另一个操作数的“同类”,这也遵循了鸭子类型精神。
4. 重载乘法运算符
4.1 重载数乘运算*
这里实现的是向量的数乘运算,我们希望
任何实数
都能和
Vector
做数乘预算(也叫做元素级乘法, elementwise multiplication),添加的两个方法如下:
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
# 以下是在控制台中运行的示例
>>> v1 = Vector([1,2,3])
>>> 2 * v1
Vector([2.0, 4.0, 6.0])
>>> v1 * True # bool是int的子类
Vector([1.0, 2.0, 3.0])
>>> from fractions import Fraction
>>> v1 * Fraction(1