Python 正确重写运算符(重载)

news2024/12/27 0:07:09

运算符重载的作用是让用户定义的对象使用中缀运算符(如+和|)和一元运算符(如-和~)。

在Python中,这些也算是运算符:

函数调用:()

属性访问:.

元素访问和切片:[]

运算符重载基础

在某些圈子中,运算符重载的名声不好,比如Java之父说过:我决定不支持运算符重载,这完全是个人选择,因为我见过太多C++程序员滥用它。

但是如果使用得当,API会变得更好用,代码变的I易于阅读。

Python施加了一些限制,做好了灵活性、可用性和安全性方面的平衡:

  • 不能重载内置类型的运算符

  • 不能新建运算符,只能重载现有的

  • 某些运算符不能重载:is、and、or、not (但是位运算符&、|、~可以)

下面依次介绍:一元运算符、中缀运算符、比较运算符、增量赋值运算符。

一元运算符

  • - (__neg__) 一元取负运算符。如果x是-2,那么-x == 2

  • + (__pos__)一元取正运算符。通常x == +x,也有一些例外

  • ~ (__invert__)对整数按位取反。定义为~x == -(x+1) ,如果x是2,那么~x ==-3

  • abs (__abs__) 取绝对值

要实现一元运算符,就实现括号中对应的特殊方法,这些特殊方法都只有一个参数self。要遵守一个原则:始终返回一个新的对象,也就是说不能修改self,要创建并返回合适类型的新实例。对于+和-来说,返回的结果可能是和self同一类型的实例;对于abs来说结果应该是一个标量;但是对于~来说,很难说返回什么是合理的,因为可能不是处理整数的位,例如在ORM中,SQL where子句应该返回反集。

示例,Vector类中实现了__abs__方法,补充上__neg__和__pos__方法

    def __abs__(self):
        """绝对值 (向量取模)"""
        return math.sqrt(sum(x * x for x in self.__components))  # math.sqrt()求平方根。先用sum求每个分量的平方之和,然后开平方
    
    def __neg__(self):
        """取负值,构建新的实例,每个分量都取反"""
        return Vector(-x for x in self)
    
    def __pos__(self):
        """取正值,构建新的实例,传入self各个分量"""
        return Vector(self)

补充下在什么时候x和+x不相等:

在Python中几乎所有情况下x == +x,但是在标准库中也有例外的情况,有两例

第一例是decimal.Decimal有关,这是因为使用+时,精度变了

import decimal

ctx = decimal.getcontext()  # 获取当前全局算术运算的上下文引用
ctx.prec = 40  # 把算术运算上下文精度设为40

one_third = decimal.Decimal('1') / decimal.Decimal('3')
print(one_third)
print(+one_third)
print(one_third == +one_third)  # 比较

ctx.prec = 28  # 把算术运算上下文精度设为28

print(one_third)
print(+one_third)
print(one_third == +one_third)  # 比较
打印
0.3333333333333333333333333333333333333333
0.3333333333333333333333333333333333333333
True
0.3333333333333333333333333333333333333333
0.3333333333333333333333333333
False

上面可以看到,在改变上下文精度后,再次与+one_third比较时就不相等了,因为+运算符会返回一个新的实例对象,而这个实例对象是根据当时的上下文精度创建的。

第二例是collection.Counter有关,Counter类实现了几个算术运算符,例如中缀运算符+的作用是把两个Counter计数器加到一起,两边相加时,负值和零值可能会改变。

而对于一元运算符+等同于加上一个空的Counter,因此他产生一个新的Counter且仅保留大于零的计数器。

import collections
ct = collections.Counter('aaaaabbrrdc')
print(ct)
ct['r'] = -3
ct['d'] = 0

# 比较
print(ct)
print(+ct)
打印
Counter({'a': 5, 'b': 2, 'r': 2, 'd': 1, 'c': 1})
Counter({'a': 5, 'b': 2, 'c': 1, 'd': 0, 'r': -3})
Counter({'a': 5, 'b': 2, 'c': 1})

我的理解是,计数器中的元素不能为负数,前面把r和d元素强制修改后,后面又通过+重新加了一个空的Counter,开始了重新计数,把小于等于零的元素都去掉了。

重载加法运算符+

中缀运算符的加法运算符 + 通过__add__实现

下面使用Vector多维向量类

...省略代码
    def __add__(self, other):
        """实现向量加法,"""
        long_zip = itertools.zip_longest(self, other, fillvalue=0.0)  # 使用zip_longest处理两个向量长度不同的情况
        return Vector(a + b for a, b in long_zip)
v1 = Vector([1, 2, 3])
v2 = Vector([3, 2, 2])
print(v1 + v2)

v3 = Vector([1])
print(v1 + v3)
打印
(4.0, 4.0, 5.0)
(2.0, 2.0, 3.0)

知识点:

  1. itertools.zip_longest方法是类似zip函数的,返回一个生成器,产生(a, b)形式的元组,如果长度不同,使用fillvalue值填充缺失的元素。

  1. 以上不仅能实现同类型相加,还可以相加任何可迭代对象,因为zip_longest接收的参数只要是可迭代就行

  1. 注意,如果左操作数是Vector之外的对象,Vector.__add__方法是无法处理的,比如(1,2,3) + v1这种会报错,其实可以使用__radd__来处理,被相加的时候调用

ps:不管是一元运算符还是中缀运算符,实现的特殊方法都一定不能修改操作数self。使用这些运算符的期待结果是新对象。只有增量运算符会就地修改第一个操作数self。

为了支持涉及不同类型的运算,Python为中缀运算符特殊方法提供了特殊的分派机制。

比如对于表达式a+b来说,解释器会执行以下操作:

  1. 如果a有__add__方法,而且返回值不是NotImplemented,会调用a.__add__(b)返回结果

  1. 如果a没有__add__方法,或者返回值是NotImplemented,然后检查b有没有__radd__方法,如果有,且没有返回NotImplemented,会调用b.__radd__(a)返回结果。

  1. 如果b没有__radd__方法,或者调用了__radd__返回了NotImplemented,结果就抛出TypeError 并在错误信息中提示操作数类型不支持

NotImplemented的含义:意为【未实现】。比如在表达式b1 == a1中,b1 .__ eq __(a1)对应的方法返回 NotImplemented,它告诉 Python 尝试调用 a1 .__ eq __(b1)

NotImplemented和NotImplementedError不要搞混:

NotImplemented是特殊的单例值,如果中缀运算符的特殊方法不能处理给它的操作数,那么就把他返回(return)给解释器。

NotImplementedError是一种异常,抽象类中的占位方法把它抛出,提醒子类必须覆盖

__radd__是__add__的“反射”(reflected)版本,或者说“反向”(reversed)版本。比如__sub__是-减法运算符,那么__rsub__就是反向减法

__radd__这种方法,是一种后备机制,如果左操作数没有实现__add__方法,或者虽然实现了,但是返回的是NotImplemented,就是表名左操作数它不知道如何处理右操作数,那么Python就会调用__radd__方法

    def __add__(self, other):
        """实现向量加法,"""
        long_zip = itertools.zip_longest(self, other, fillvalue=0.0)  # 使用zip_longest处理两个向量长度不同的情况
        return Vector(a + b for a, b in long_zip)

    def __radd__(self, other):
        """实现向量加法,"""
        return self + other  # 委托给__add__处理

以下的操作会抛出异常,

v1 = Vector([1, 2, 3])

print(v1 + 'ABC')

TypeError: unsupported operand type(s) for +: 'float' and 'str'

这是因为__add__方法的操作里面有了错误,应该抛出NotImplemented才对,而不是TypeError。如果返回NotImplemented,另一个操作数还有机会执行运算,Python会尝试调用反向运算符方法。

我们应该在__add__方法中捕获异常,然后返回NotImplemented。如果反向方法还是返回NotImplemented,这时Python才会抛出TypeError。如果直接不捕获异常,就终止了运算符分派机制。

最终版本:

    def __add__(self, other):
        """实现向量加法,"""
        try:
            long_zip = itertools.zip_longest(self, other, fillvalue=0.0)  # 使用zip_longest处理两个向量长度不同的情况
            return Vector(a + b for a, b in long_zip)
        except TypeError:
            return NotImplemented

    def __radd__(self, other):
        """实现向量加法,"""
        return self + other

重载乘法运算符*

对于Vector([1,2,3]) * 10这是计算标量积(scalar product),会把每个分量乘以10,也叫作叫元素级乘法(elementwise multiplication)

示例,实现乘法

...s省略代码    
    type_code = 'd'  # 类属性。在于实例和字节序列转换时使用。

    def __init__(self, components):
        self.__components = array(self.type_code, components)
        
    def __mul__(self, other):
        if isinstance(other, numbers.Real):  # 限制不能使用复数
            return Vector(other * x for x in self)
        else:
            return NotImplemented

    def __rmul__(self, other):
        return self * other  # 委托给__mul__
    
v1 = Vector([1, 2, 3])

print(v1 * 10)
print(100 * v1)
打印
(10.0, 20.0, 30.0)
(100.0, 200.0, 300.0)

知识点:

  1. 因为Vector内部使用的是浮点数数组,所以相乘要保证是非复数,可以是int、bool(int的子类),甚至是fraction.Fraction(分数)

还有一种,分量乘以分量,然后相加。叫做两个向量的点积(dot product)

在Python3.5中,这种操作使用@运算符,意为 矩阵乘法(matrix multiplication),使用特殊方法__matmul__、__rmatmul__和__imatmul__实现。

示例,实现@运算符,矩阵乘积

...代码省略
    def __matmul__(self, other):
        try:
            return sum(a * b for a, b in zip(self, other))  # 实测不能使用zip_longest,fillvalue=1,  参考numpy中不对称的矩阵乘积会报错才对
        except TypeError:
            return NotImplemented

    def __rmatmul__(self, other):
        return self @ other
    
v1 = Vector([1, 2, 3])
v2 = Vector([5, 6, 7])


print(v1 @ v2)
打印
38
中缀运算符一览

上面提到的加法+和乘法* 都属于中缀运算符

补充举例:

a |= 2等价于a = a|2(按位或)

a>>=2等价于a=a>>2(右移3位)

a<<=2等价于a=a<<2(左移3位)

比较运算符

Python解释器中 比较运算符和前面介绍的中缀运算符类型,正反向返回NotImplemented的话,调用反向方法。

但是有两点不同:

  • 正向和反向使用的都是同一系列方法,只是把参数对调了。比如对于==来说,正向反向都是调用__eq__方法,只是参数对调;而正向的__gt__方法调用的是反向的__lt__方法,并把参数对调。

  • 对于==和!=来说,如果反向调用失败,Python会比较对象的ID,而不是抛出TypeError。

Python3的改动:

  • 在Python3中,对于__ne__来说,会直接返回__eq__结果的相反。

  • Python3对于不同类型的比较运算符,能正常抛出异常TypeError: '>' not supported between instances of 'int' and 'tuple'

然而在Python2中,会比较对象的类型和id,返回结果是FALSE。(整数和元组比较显然无意义,Python2的做法不合理)

示例,完善Vector类的__eq__方法。

...省略代码
    def __eq__(self, other):
        """实现可散列,判断相等性"""
        if isinstance(other, Vector):  # 如果other操作数是Vector实例,
            return len(self) == len(other) and all(a == b for a, b in zip(self, other))  # 先比较长度相等,再比较分量相等
        else:
            return NotImplemented
        
v1 = Vector([1, 2, 3])
print(v1 == (1, 2, 3))
打印
False

v1 == (1, 2, 3)比较的过程:

  1. 为了计算v1==(1,2,3),Python调用Vector.__eq__(v1, (1,2,3))

  1. 经过Vector.__eq__方法确认(1,2,3)元组不是Vector实例,因此返回NotImplemented

  1. Python得到NotImplemented后,尝试调用tuple.__eq__((1,2,3), v1)

  1. tuple.__eq__方法不知道Vector是什么,因此返回了NotImplemented

  1. Python得知反向调用也返回了NotImplemented,就会比较对象的ID,作为最后一博。

对于!=运算符,我们不用实现,因为从object继承的__ne__方法的后备行为,__ne__方法会对__eq__返回的结果取反。

object的__ne__方法,与下面代码类似,不过原版是通过C语言实现的:

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

不过这种处理办法也有缺陷,x==y成立,不带便x!=y不成立。在Python2中,如果定义了__eq__方法,那么做好定义__ne__方法,这样运算符的行为才能符合预期。但是在Python3中,从object中继承的__ne__方法就够用了,技术不用重载。

增量赋值运算符

如果一个类,没有实现就地运算符,如__iadd__/__imul__等等,增量赋值运算符只能算是语法糖:a += b作用和a = a+b完全一致,对于不可变类型来说,这是正常的行为,并且如果实现了__add__方法,不需要额外的代码,+=就能实现

比如Vector类,就是不可变类型,对于+=或者*=运算符,会产生新的实例,查看实例ID,会发现id变化。

与+相比,+=运算符对于第二个操作数更宽容。+运算符的两个操作数必须是相同类型,而+=的情况更明确,因为就地修改左操作数,所以结果的类型是确定的,一定的左操作数的类型。

比如内置类型list,对于+运算符是把两个列表加到一起,而my_list += x 是把右边可迭代对象中的元素扩展到左边的列表,这和list.extend()行为相同,它的参数可以是任何可迭代对象。

示例,AddableBingoCage扩展BingoCage,支持+和+=

class AddableBingoCage(BingoCage):
    def __add__(self, other):
        if isinstance(other, Tombola):  # +运算符需要保证两个操作数的同类型
            return BingoCage(self.inspect() + other.inspect())
        else:
            return NotImplemented

    def __iadd__(self, other):
        if isinstance(other, Tombola):
            other_iterable = other.inspect()
        else:
            try:
                other_iterable = iter(other)  # 尝试把右操作数创建迭代器,这里可能会报异常
            except TypeError:
                class_name = type(self).__name__
                # class_name = self.__class__.__name__  或者这样
                msg = "right operand in += must be {!r} or an iterable"
                raise TypeError(msg.format(class_name))

        self.load(other_iterable)
        return self  # 增量赋值的特殊方法,必须返回self


a = AddableBingoCage([1, 2, 3])
b = AddableBingoCage([5, 6, 7])

c = a + b
print(c.inspect())

a += b
print(a.inspect())

a += [8, 9]
print(a.inspect())

a += 123
print(a.inspect())
打印
(1, 2, 3, 5, 6, 7)
(1, 2, 3, 5, 6, 7)
(1, 2, 3, 5, 6, 7, 8, 9)
Traceback (most recent call last):
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/Tombola.py", line 114, in __iadd__
    other_iterable = iter(other)  # 尝试把右操作数创建迭代器,这里可能会报异常
TypeError: 'int' object is not iterable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/Tombola.py", line 137, in <module>
    a += 123
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/Tombola.py", line 119, in __iadd__
    raise TypeError(msg.format(class_name))
TypeError: right operand in += must be 'AddableBingoCage' or an iterable

知识点:

  1. 实现增量赋值的特殊方法,最后一定要返回self

  1. __add__方法,要保证两个操作数是同一类型,要创造一个新的实例作为结果返回

  1. __iadd__方法,最后把修改后的self作为结果返回

一般来说,如果中缀运算符的正向方法只处理与self 同一类型的操作数,那么就无需实现对应的反向方法,因为按照定义,反向方法是为了处理类型不同的操作数。

小结

虽然都是用+-符号,但是在一元运算符中

-表示__neg__

+表示__pos__

在中缀运算符中

-表示__sub__

+表示__add__

  • 一元运算符和中缀运算符的结果都应该产生新的对象,而且绝不能修改操作数。

  • 为了支持其他类型,返回特殊的NotImplemented值,让解释器尝试对调操作数,然后调用运算符的反向特殊方法。

  • 如果操作数类型不同,有两种方式处理:1.鸭子类型,直接执行运算,捕获TypeError。2.使用isinstance测试,主要要使用抽象基类,而不是具体类

  • 使用__eq__实现了==后,Python会通过__ne__方法对__eq__取反作为结果判断!=运算符。最后还有后备机制,判断对象ID。

  • +=对于可变对象,没有产生新对象。对于不可变对象,会产生新的对象。

  • + 两边的操作数必须是同一个类型。+=对于右操作数一般是任何可迭代对象。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/190544.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RabbitMQ消息转换器

文章目录RabbitMQ消息转换器RabbitMQ消息转换器 在SpringAMQP的发送方法中&#xff0c;发送消息和接受消息的类型都是Object&#xff0c;也就是说&#xff0c;我们可以发送任意对象类型的消息&#xff0c;SpringAMQP都会帮我们把发送的消息序列化为字节后再进行发送。下面&…

剑指 Offer 第 16 天 把数组排成最小的数 扑克牌中的顺子

目录 面试题45. 把数组排成最小的数 面试题61. 扑克牌中的顺子 面试题45. 把数组排成最小的数 输入一个非负整数数组&#xff0c;把数组里所有数字拼接起来排成一个数&#xff0c;打印能拼接出的所有数字中最小的一个。 示例 1: 输入: [10,2] 输出: "102" 示例 2: 输…

会声会影2023最新版v25.3.0.0视频剪辑软件

各位新年好呀&#xff0c;2022年的经历让我们更加成长&#xff0c;我们也终于迎来了新的一年。在这里&#xff0c;COCO玛奇朵先祝大家新年快乐&#xff0c;万事如意&#xff01; 会声会影是一款操作简单易学的视频剪辑软件&#xff0c;会声会影的编辑步骤是流程化的&#xff0…

lamp的简单搭建 小白笔记

笔记是观看哔哩哔哩视频所写。链接如下&#xff1a;经典LAMP架构-跟着老王学Linux_哔哩哔哩_bilibili笔记有多处不妥之处&#xff0c;本是自己写笔记加深印象&#xff0c;仅供参考。所用linux系统是CentOS7。注意&#xff1a;该笔记操作后的网站仅主机内的局域网可以访问&#…

【数学建模】常用算法-线性回归Python实现

1 前言 本文主要讲解基于线性回归的糖尿病预测的python实现&#xff0c;后续会进行进一步的更新 2 代码实现 2.1 数据准备 导入相关的包 import numpy as np import pandas as pd加载数据集 这个数据集是sklearn.datasets自带的糖尿病数据集&#xff08;diabetes&#xff…

I.MX6ULL内核开发2:内核模块实验2

目录 一、模块参数 二、符号共享 三、模块自动卸载 四、关于Makefile的说明 一、模块参数 根据不同应用场合给内核模块传递不同的参数&#xff0c;提高内核模块灵活性 定义一个常见变量使用module_param宏把传参值赋给变量module_param(name,type,perm) name: 参数名type…

uniapp弹幕

效果图 barrage.vue <template><view class"l-barrage"><block v-for"(item,index) in items" :key"index"><view v-if"item.display" class"aon" :style"{top: ${item.top}rpx}"><…

数据库丨记录一次TiDB v5.2.3迁移到v6.1.0的实操过程

文章目录前言一、环境简介二、迁移前提条件三、迁移方案四、迁移过程总结前言 TiDB 是一款结合了传统的关系型数据库和 NoSQL 数据库特性的新型分布式数据库。是第一个把数据分布在全球范围内的系统&#xff0c;并且支持外部一致性的分布式事务。 TiDB 具备强一致性和高可用性…

Struts2之注解

Struts2之注解1、引入依赖2、注解结构3、注解的使用1、引入依赖 Struts2中可以使用注解来代表struts.xml中的某些配置&#xff0c;可以简化配置。要使用注解&#xff0c;必须引入额外的依赖&#xff0c;如下&#xff1a; <!-- struts2注解依赖 --><dependency><…

未经风雨,怎见彩虹!回首2022,你好2023,新的一年,点面科技与您一起大展宏“兔”,扬眉“兔”气!

时光飞逝&#xff0c;转眼间2022虎年即将变成昨天的记忆。2022年&#xff0c;对于整个行业而言都是不寻常的一年&#xff0c;我们砥砺艰辛&#xff0c;着实经历着一场“寒冬”。但是&#xff0c;2022年注定也是不平凡的一年&#xff0c;在这个变中有定、变中求新的市场格局下&a…

【Rust】11. 泛型、Trait 和生命周期

11.1 泛型数据类型 11.1.1 函数的泛型 注意&#xff1a;泛型的比较适用于实现了 std::cmp::PartialOrd trait 的数据类型 11.1.2 结构体的泛型 结构体的泛型可以使用多个泛型类型参数 11.1.3 枚举的泛型 类似于结构体&#xff0c;枚举的泛型也可以使用多个泛型类型参数 11.…

Android 动画

在App中合理地使用动画能够获得友好愉悦的用户体验&#xff0c;Android中的动画有View动画、属性动画、帧动画、布局动画、转场动画等&#xff0c;在5.x以后有又新增了矢量动画&#xff0c;这些动画在平常开发中使用较为普遍&#xff0c;所以有必要做一次完整的总结。一、View动…

130道python练习题 完整版PDF

今天跟大家分享一些干货&#xff0c;在学python的朋友可以动起来了&#xff01; python基础知识练习题&#xff0c;常见常用的&#xff0c;可以作为参考&#xff0c;挺不错的&#xff0c;也有许许多多的讲解&#xff0c;适合python巩固基础知识和入门 130道练习题&#xff0c…

电脑网速慢怎么解决?提升网速真的很容易!

我们经常会使用电脑&#xff0c;如果加载网页的时间过长&#xff0c;或者是出现未响应的提示等问题&#xff0c;会非常影响我们使用电脑的观感。 针对这种问题&#xff0c;电脑网速慢怎么解决&#xff1f;来看看下面造成电脑网速慢的主要原因&#xff0c;以及我们该怎么提升电…

Python父类方法重写

在 Python 中&#xff0c;子类继承了父类&#xff0c;那么子类就拥有了父类所有的类属性和类方法。通常情况下&#xff0c;子类会在此基础上&#xff0c;扩展一些新的类属性和类方法。但凡事都有例外&#xff0c;我们可能会遇到这样一种情况&#xff0c;即子类从父类继承得来的…

vue前框框架课程笔记(六)

目录单文件组件.vue文件单文件组件常用结构App.vueStudent.vueSchool.vuemain.jsindex.html运行结果vue脚手架具体步骤项目架构其他配置项props配置项mixin混入插件scoped属性本博客参考尚硅谷官方课程&#xff0c;详细请参考 【尚硅谷bilibili官方】 本博客以vue2作为学习目…

zookeeper源码分享四 ---- RequestProcessor 处理链路

单机zookeeper RequestProcessor 处理链路 将请求放入LinkedBlockingQueue队列中&#xff0c;通过一个队列中。启动一个线程去消费这个队列&#xff0c;避免了阻塞。 zookeeper的处理是实现RequestProcessor接口的processRequest(Request request) 方法。 PrepRequestProcesso…

exsi删除虚拟机提示在当前状况下不允许执行此操作解决方法、vmware删除虚拟机提示在当前状况下不允许执行此操作解决方法

说明 我这exsi版本为5.5 今天在回收虚拟机的时候有些虚拟机无法删除&#xff0c;提示下面内容。 解决方法 方法1 直接给删除失败的虚拟机开机&#xff0c;开机完毕以后再关机就能直接删除了。 方法2 删除失败的虚拟机&#xff0c;也无法开机的情况下&#xff0c;实用该方…

春节档的观影“热”,拯救不了影视圈的“冷”?

配图来自Canva可画 疫情三年&#xff0c;影视业也随着“冬眠”了三年。 先是疫情爆发影响影视业上下游的正常生产&#xff0c;而后国家加速规范影视行业税收秩序&#xff0c;资本相继撤离&#xff0c;再后来影视企业入不敷出业绩巨亏&#xff0c;影视寒冬来临成为行业共识。在…

MMCV - dataset_analysis.py 可视化检测和跟踪任务自定义数据集神器

做视觉检测跟踪任务时,我们需要在论文插图中体现出我们数据集的信息,这个时候就有一个非常好用的神器:dataset_analysis.py的文件。该文件能够帮助用户直接可视化custom数据集的bbox实例信息,如上图所示,包括:显示类别和 bbox 实例个数的分布图;显示类别和 bbox 实例宽/…