1.运算符重载基础
运算符重载的作用是让用户使用中缀运算符(如:+和|)和一元运算符(如:-和~)。说得宽泛些,在Python中,函数调用(())、属性访问(.)和元素访问/切片([])也算是运算符,不过本章只讨论一元运算符和中缀运算符。
因为在某些圈子中,运算符重载的名声并不好。所以Python对其进行了限制:
(1)不能重载内置类型的运算符。
(2)不能新建运算符,只能充在现有的。
(3)某些运算符不能重载——is、and、or、not。
2.一元运算符
在Python中,一元运算符如下面表格,分为三种。
-(_neg_) | 一元取负算术运算符,eg:x=2,-x==-2 |
+(_pos_) | 一元取正算术运算符,eg:x=2,+x==2 |
~(_invert_) | 对整数按位取反,定义为-x=-(x+1) |
abs(...)(_abs_) | 取绝对值 |
支持一元运算符很简单,只需要实现相应的特殊方法即可 。这些特殊方法只有一个参数self。然后,使用符合所在类的逻辑实现。不过要遵守运算符的一个基本规则:始终返回一个新对象。也就是说,不能修改self,要创建并返回合适类型的新实例。
还是对以前讲过的Vector实例进行完善,添加_neg_、_pos_和_abs_一元运算符方法。
def __abs__(self):
return math.sqrt(sum(x * x for x in self))
def __neg__(self):
return Vector(-x for x in self)
def __pos__(self):
return Vector(self)
可以发现,abs()方法返回的是一个标量,而neg和pos方法返回的是一个新的Vector实例。
3.重载向量加法运算符
两个欧几里得向量加到一起得到的是一个新的向量,它的各个分量是两个向量中相应的分量之和。但是,要注意一个问题,如果两个向量长度不相等时,可能会抛出错误,此时,我们最好使用零来填充那个较短的向量。
def __add__(self, other):
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a+b for a, b in pairs)
pairs是个生成器,它会生成(a,b)形式的元组,a来自self,b来自other。并且,当两个向量长度不相等时,会用0来填充那个较短的可迭代向量。然后,构建一个新Vector实例,使用生成器表达式来计算各元素的和。
注意,实现一元运算符和中缀运算符的特殊方法一定不能修改操作数。使用这些运算符的表达式期待结果是个新对象。只有增量赋值表达式可能会修改第一个操作数(self)。
但是,当我们对调操作数,混合类型的加法就会失败。为了支持涉及不同类型的运算,Python为中缀运算符特殊方法提供了特殊的分派机制,以a+b表达式来进行举例:
(1)如果a有_add_方法,而且返回值不是NotImplemented,调用a._add_(b),然后返回结果。
(2)如果a没有_add_方法,或者返回值是NotImplemented,检查b有没有_radd_方法,如果有,并且没有返回NotImplemented,就调用b._radd_(a),然后返回结果。
(3)如果b没有_radd_方法,或者调用_radd_方法返回NotImplemented,就会抛出TypeError错误,并在错误消息中指明操作数类型不支持。
下面,我们将为Vector实例加上_radd_方法。
def __radd__(self, other):
return self+other
为了遵守鸭子类型精神,我们不能测试other操作数的类型,或者它的元素类型。我们要捕获异常,然后返回NotImplemented。下面是Vector实例加法的特殊方法的最终方法。
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
4.重载标量乘法运算符*
Vector([1, 2, 3])* x是什么意思?如果x是数字,就计算标量积,结果是一个新的Vector实例,各分量都会乘一个x——这也叫元素级乘法。另外,如果x也会是一个Vector实例,那么这个乘法叫做点积,就是对应元素进行想乘,也就是矩阵乘法。下面我们先进行简单的实现。
def __mul__(self, other):
return Vector(n * other for n in self)
def __rmul__(self, other):
return other * self
这两个方法确实可用,但是提供不兼容的操作数时,会出现问题。所以,我们采用之前讲到的“白鹅类型”,使用isinstance检查other的类型,但不硬编码具体的类型,而是检查numbers.Real抽象基类。这个抽象基类几乎涵盖了我们所需的全部类型。
def __mul__(self, other):
if isinstance(other, numbers.Real):
return Vector(n * other for n in self)
else:
return NotImplemented
def __rmul__(self, other):
return other * self
在mul方法中使用isinstance检查other是否是numbers.Real的某个子类的实例,用分量的乘积创建一个新的Vector实例,否则,返回NotImplmented。
下面简要介绍下常用的中缀运算符:
运算符 | 说明 |
+ | 加法或拼接 |
- | 减法 |
* | 乘法或重复复制 |
/ | 除法 |
// | 整除 |
% | 取模 |
divmod() | 返回由整除的商和模数组成的元组 |
**,pow() | 取幂* |
@ | 矩阵乘法 |
& | 位与 |
| | 位或 |
^ | 位异或 |
<< | 按位左移 |
>> | 按位右移 |
5.众多比较运算符
Python解释器对众多比较运算符的处理与前文类似,不过有以下两部分的区别:
(1)正向和反向调用使用的是同一系列的方法。如:对==来说,正向和反向调用都是_eq_方法,只是把参数对调了,而正向的_gt_方法调用的是反向的_lt_方法,并把参数对调。
(2)对==和!=来说,如果反向调用失败,Python会比较对象的ID,而不会抛出TypeError。
当我们再次回过头来看Vector实例中的_eq_方法时,会发现它并不完美。因为它会判断一个Vector实例与一个元组相等。所以要进行改进。
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
6.增量运算符
其实到现在,Vector类已经支持增量赋值运算符*=和+=了。要注意的是,增量赋值不会修改不可变目标,而是新建实例,然后重新绑定。
如果一个类没有实现就地运算符,增量赋值运算符只是语法糖,a+=b的作用与a=a+b的作用完全一样。对不可变类型来说,这是预期的行为,而且,如果编写了_add_方法的话,不用编写额外的代码,+=可以直接用。
但是如果你定义了就地运算符方法,例如:_iadd_,计算a+=b的结果时会调用就地运算符方法。这种运算符的名称表明,他们会就地修改左操作数,而不会创建新的对象作为结果。
此外,值得一提的是,+=比+运算符更宽容。+运算符的两个操作数必须是相同类型,如若不然,结果的类型可能会出乎人的意料。而+=的情况就更加明确,因为就地修改左操作数,所以结果的类型是确定的。