Python编程:正确重载运算符

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的结果时会调用就地运算符方法。这种运算符的名称表明,他们会就地修改左操作数,而不会创建新的对象作为结果。

        此外,值得一提的是,+=比+运算符更宽容。+运算符的两个操作数必须是相同类型,如若不然,结果的类型可能会出乎人的意料。而+=的情况就更加明确,因为就地修改左操作数,所以结果的类型是确定的。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值