流畅的python笔记(十三)正确重载运算符

目录

一、运算符重载基础

二、一元运算符

- + ~ abs()

x和+x何时不相等

与decimal.Decimal类有关的情况

与collections.Counter有关的情况

三、重载向量加法运算符+

四、重载标量乘法运算符*

五、比较运算符

六、增量赋值运算符


一、运算符重载基础

python对运算符重载加了一些限制。

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

二、一元运算符

- + ~ abs()

一元运算符比如 - + ~等,每个一元运算符都对应一个特殊方法,abs()函数也是一元运算符,对应的特殊方法是__abs__。支持一元运算符只需要实现相应的特殊方法即可,这些特殊方法只有一个参数self。运算符基本规则:始终返回一个新对象,即不能修改self。即便是+,最好也是返回一个副本,不能返回引用。

 

  1.  为了计算-v,构建一个新的Vector实例,把self的每个分量都取反。
  2.  为了计算+v,构建一个新的Vector实例,传入self的各个分量。

x和+x何时不相等

与decimal.Decimal类有关的情况

  1. decimal.getcontext获取当前全局算术运算的上下文引用
  2. 把算术运算上下文的精度设为40
  3. 使用当前精度计算1/3
  4. 查看结果,小数点后有40个数字
  5. one_third == +one_third返回True
  6. 把精度降低为28,这是python3.4为Decimal算术运算设定的默认精度
  7. 现在,one_third == +one_third返回False
  8. 查看+one_third,小数点后有28个数字

one_third小数点后边始终有40个数字,第一个+one_third表达式返回的结果小数点后边也有40个数字,而第二个+one_third表达式返回结果的时候,当前算术运算上下文的精度已经变了,因此返回结果后边只有28个表达式,因此与one_third不同了。

与collections.Counter有关的情况

Counter实现了中缀运算符+,作用是把两个Counter的实例加一起,但是相加之后的结果会剔除掉零值和负值。对于Counter,一元运算符+等同于加上一个空Counter,因此会产生一个新的Counter且仅保留大于零的计数器。

三、重载向量加法运算符+

这一小节中的+是中缀运算符。

对于序列类型,+一般用于拼接,而*用于重复复制。但对于向量的说应该用向量加法才对。

如果两个Vector实例长度不同,则用零填充较短的向量。

满足以上要求的实现如下:

  1. pairs是一个生成器,可以生成(a, b)形式的元组,其中a来自self,b来自other。如果self和other的长度不同,使用fillvalue填充较短的那个可迭代对象。
  2. 构建一个新的Vector实例,使用生成器表达式计算pairs中各个元素的和。4
  3. 这种实现方式可以把Vector加到任何生成数字的可迭代对象上,但是如果对调操作数,用可迭代对象来加Vector,则会失败,因为python中缀运算符对应的特殊方法提供了特殊的分派机制。

对于表达式a+b,python解释器会执行以下几步操作:

__radd__是__add__的反向版本,可称为反向特殊方法,因为它是在右操作数上调用的,同理__rsub__也是反向特殊方法(也可叫右向特殊方法,r是right的意思)。因此为了让上边+的实现能支持右操作数是Vector,左操作数是生成数字的可迭代对象,只需要再实现__radd__即可。

        最简可用的__radd__实现如下:

上边重载的+支持可迭代对象与Vector之间的运算,但是如果提供的对象不可迭代,则__add__无法处理。

即使操作数是可迭代对象,但是它的元素不是数字,则还是不能与Vector中的浮点数元素相加。

如果由于类型不兼容导致运算符特殊方法失效,不应该返回TypeError,应该返回NotImplemented,返回NotImplemented时,另一个操作数所属类型还有机会执行运算,即python会尝试调用反向方法。

        下面是最终实现方案:

四、重载标量乘法运算符*

标量乘法,即元素级乘法,用标量乘向量中的每个分量,如下例子所示:

*运算符用于计算标量积,其对应的特殊方法是__mul__和__rmul__。

以上实现中,因为Vector内部是浮点数数组,任何数字乘以浮点数结果都是浮点数,因此scalar可以是int、bool、甚至fractions.Fraction实例等标量,但是不能是复数。为了判断scalar我们可以采用白鹅类型:使用isinstance()检查scalar的类型,不硬编码具体的类型,而是检查numbers.Real抽象基类,而且还支持以后声明为numbers.Real抽象基类的真实子类或虚拟子类的数值类型。

  1. 为了使用Real抽象基类,导入numbers模块。
  2. 如果scalar是numbers.Real的某个子类的实例,则进行标量乘积并返回一个新的Vector实例。
  3. 如果scalar不是numbers.Real的实例,返回NotImplemented,这样python会尝试在scalar上调用__rmul__方法。
  4. __rmul__方法定义,有了__rmul__,Vector就可以作为右操作数进行标量数乘。

这样,Vector类型实例就可以乘以很多的数字类型了,如下例子:

以下是python中中缀运算符列表:

 

五、比较运算符

pythone解释器对比较运算符(== != > < >= <=)的处理与前边提到的+,*等在两个方面有重大区别:

  1. 正向调用和反向调用使用同一方法,比如==,正向调用和反向调用都是__eq__方法,只不过把参数对调了,对于>=,正向调用的__gt__方法内部是委托给反向的__lt__方法来实现的,也是参数对调。
  2. 对于==和!=来说,如果反向调用失败,python会比较对象的ID,而不抛出TypeError。

接下来对前边几章中Vector.__eq__方法进行改进,原实现如下:

这会导致一个结果,任何可迭代对象,只要其分量与Vector实例相等,则==就判定他们相等。

为了解决这个问题,我们需要对操作数进行类型检查。

 

  1.  如果第二个操作数是Vector的实例或Vector的子类的实例,则正常进行比较。
  2.  否则,返回NotImplemented。

再看测试例子:

  1. 两个Vector实例,符合预期
  2. 判断正确,但是原因需要后文分析
  3. 有一个操作数是元组实例,因此判断False

在第二个测试例子中,Vector实例与Vector2d比较时,具体步骤如下:

  1. 为了计算vc == v2d,python调用Vector.__eq__(vc, v2d)
  2. 经Vector.__eq__(vc, v2d)确认,v2d不是Vector实例,因此返回NorImplemented
  3. python得到NotImplemented,尝试调用Vector2d.__eq__(v2d,vc)
  4. Vector2d.__eq__(v2d, vc)把两个操作数都变成元组,然后比较,结果是True

在第三个测试例子中,Vector实例和元组比较时,步骤如下:

  1. 为了计算va == t3,python调用Vector.__eq__(va, t3)
  2. 经Vector.__eq__(va,t3)确定,t3不是Vector实例,因此返回NotImplemented。
  3. python得到NotImplemented结果,尝试调用tuple.__eq__(t3, va)
  4. tuple.__eq__(t3, va)不知道Vector是什么,因此返回NotImplemented
  5. 对 == 来说,如果反向调用返回NotImplemented,python会比较对象的ID,作最后一搏。

对于!=运算符,其对应的__ne__方法会对__eq__返回的结果取反。

六、增量赋值运算符

增量赋值运算符不会改变不可变目标,而是新键实例,然后重新绑定变量。

  1. 赋值一份,v1_alias是v1对应对象的引用,id(v1_alias)等于id(v1)
  2. 这里是最开始绑定给变量v1的Vector实例的ID
  3. 增量加法运算
  4. v1的结果可以看到元素加法成功
  5. 但是通过查看id发现绑定给v1的是新的Vector实例
  6. 审查v1_alias,确认原来的Vector实例没被修改
  7. 增来乘法运算
  8. 同样,创建了新的Vector实例

如果一个类没有实现就地运算符对应的特殊方法比如__iadd__,那么增量赋值运算符只是语法糖,a += b与a = a + b完全等价。对不可变类型来说,只要定义__add__,+=直接就能用。但是如果实现了就地运算符方法,比如__iadd__,则计算a += b的时候会调用就地运算符方法,不会创建新实例而是修改左操作数。不可变类型,如Vector类,一定不能实现就地特殊方法

        注意,与+相比,+=运算符对第二个操作数更宽容,很多时候 + 运算符的两个操作数必须是相同类型(但也有些时候第二个可以不是),而+=是就地修改左操作数,结果类型是确定的,所有相当于是用右操作数扩展左操作数,比如对于序列类型,一般可迭代对象就可以做右操作数。

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值