Python基础之正确重载运算符

导语:本文章记录了本人在学习Python基础之面向对象篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。

本文重点:

1、掌握运算符重载的定义和作用,以及Python对其的内部限制;
2、掌握一元运算符重载设计思路;
3、理解中缀运算符重载过程中鸭子类型和白鹅类型思想的运用并掌握。

一、运算符重载基础

运算符重载:对已有的运算符进行重新定义,赋予其另一种功能,以适应不同的数据类型。
重载的作用:令用户定义的对象能够使用中缀运算符(如 + 和 | )或一元运算符(如 - 和 ~ )等运算符。
为了做好灵活性、可用性和安全性方面的平衡,Python对运算符重载施加了一些限制:

  • 不能重载内置类型的运算符
  • 能新建运算符,只能重载现有运算符
  • 某些运算符不能重载,如is、and、or和not(不过位运算符&、| 和 ~可以)

二、一元运算符

1、常见的一元运算符

  • -(__neg__),一元取负算术运算符。例:若 x 是 -2,则 -x==2。
  • +(__pos__),一元取正算术运算符。通常x==+x。
  • ~(__invert__),对整数按位取反,~x== -(x+1)。例:若 x 是 -2,则 ~x==1。

另外,Python语言参考手册将内置的abs()函数列为一元运算符,它对应的特殊方法是__abs__。

2、重载一元运算符

重载一元运算符只需实现相应的特殊方法,这些特殊方法只有self一个参数。
重载应遵循运算符的一个基本规则:始终返回一个新对象
即,不能修改self,要创建并返回合适类型的新实例。

下面我们以第10章的多维向量类为例重载一元运算符:

import math
class Vector:
#排版需要省略中间代码
    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)

    def __invert__(self):
        return Vector(-x-1 for x in self)

3、x和+x不相等的情况

  • 算术运算上下文的精度变化可能导致 x 不等于 +x

Python基础之正确重载运算符
Python 3.4 为 Decimal 算术运算设定的默认精度是28,这里因为+x使用上下文的精度导致相等性判断返回False。

  • counter实例不含零值和负值计算器

Python基础之正确重载运算符
通过上面的实例能够看到counter实例ct经过零值和负值的赋值之后,再经过+x运算后发现ct实例中的非负数对象均消失了。事实上一元运算符 + 等同于加上一个空 Counter。当Counter相加时,Python解释器从实用性角度出发会把负值和零值的计数从结果中剔除。

三、中缀运算符

1、重载加法__add__

现在我们仍以第10章的多维向量为例进行中缀运算符加号“+”的重载。
重载加法的目标分析

  • 当多维向量类是操作数时,多维向量应支持与同类向量的加法
  • 同时多维向量类还应支持与可迭代对象的加法
  • 此外当可迭代对象是操作数的时候,多维对象应具备__radd__如此来调用多维向量类中的__add__方法

重载加法的流程图设计:
设计的重点在于采用鸭子类型思想。当多维向量类与非数值类相加时,多维向量类无法处理异类加法运算可以将加法运算交给右操作数的类处理。因为右操作数存在可以处理这种异类加法的可能。
Python基础之正确重载运算符

重载加法的代码实现:

from itertools import zip_longest
class Vector:
#排版需要省略中间代码
    def __add__(self, other):
        try:
            return Vector(a+b for a,b in zip_longest(self,other,fillvalue=0))
        except TypeError:
            return NotImplemented
    def __radd__(self, other):
        return self+other

2、重载乘法__mul__
重载加法的目标分析

  • 当多维向量类是操作数时,多维向量应支持与同类向量的乘法
  • 同时多维向量类还应支持与可迭代对象的加法
  • 此外当可迭代对象是操作数的时候,多维对象应具备__rmul__如此来调用多维向量类中的__mul__方法

注意:我们对多维向量重载的乘法是针对数论中的实数类型进行运算,此时可以采用白鹅类型显式检查对象的抽象基类是否为numbers.Real,代码实现如下:

import numbers 
class Vector:
#排版需要省略中间代码
    def __mul__(self, other):
        if isinstance(other,numbers.Real):
                return Vector(x*other for x in self)
        else:
            return NotImplemented
    def __rmul__(self, other):
        return self*other

Tips:一般来说只有当处理与self不同类型的操作数时,需要创建反向方法处理。否则没有必要创建反向方法。

四、比较运算符

Python 解释器对众多比较运算符(==、 !=、 >、 <、 >=、 <=) 的处理与前文类似, 不过在两个方面有重大区别。

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

Python基础之正确重载运算符

1、重载等号__eq__

现在我们仍以第10章的多维向量为例进行中缀运算符等号“=”的重载。
重载等号的返回为True的条件

  • 等号两端对象为同类对象
  • 等号两端对象中的每个元素都必须对应相等

注意:若Vector处理等号不为True,应该返回NotImplemented交由Python处理。如果反向调用返回 NotImplemented,Python 会使用后备机制比较对象的 ID,作最后一搏。

重载等号的代码实现如下:

class Vector:
#排版需要省略中间代码
    def __eq__(self, other):
        if isinstance(other,Vector):
            return len(self)==len(other) and all(x==y for x,y in zip(self,other))
        else:
            return NotImplemented

2、了解object中__ne__的实现

def __ne__(self, other):
    eq_result = self == other
    if eq_result is NotImplemented:
        return NotImplemented
    else:
        return not eq_result

五、增量赋值运算符

1、重载加等于__iadd__

现在我们仍以第10章的多维向量为例进行中缀运算符加等于“+=”的重载。
重载加等于设计要求

  • 加等于右侧的对象与左侧的Vector类是同类对象或可迭代对象
  • 否则抛出TypeError,显示无法进行加等于计算

下面以BingoCage的子类AddableBingoCage为例实现__iadd__,大家不必在意这个子类,重点在于理解__iadd__实现的思路:

import itertools 
from tombola import Tombola
from bingo import BingoCage
class AddableBingoCage(BingoCage): 
    def __add__(self, other):
        if isinstance(other, Tombola): 
            return AddableBingoCage(self.inspect() + other.inspect()) 
            #self.inspect()继承自BingoCage,返回当前元素组成的有序元组。
        else:
            return NotImplemented
    def __iadd__(self, other):
        if isinstance(other, Tombola):
            other_iterable = other.inspect() 
        else:
            try:
                other_iterable = iter(other)
            except TypeError: 
                self_cls = type(self).__name__
                msg = "right operand in += must be {!r} or an iterable"
                raise TypeError(msg.format(self_cls))
        self.load(other_iterable) 
        return self