重载内置运算符
改变运算符的行为与改变函数的行为一样简单。在类中定义其相应的特殊方法,运算符就会根据这些方法中定义的行为进行工作。
这些不同于上述特殊方法的意义是,除了self外他们需要接受另一参数,一般用other来指代。让我们看看几个例子。
使对象能够使用 + 进行加操作
与运算符 + 对应的特殊方法是__add__方法。添加自定义__add__会改变运算符的行为。建议__add__返回类的新实例,而不是修改调用实例本身。在 Python 中这种行为很常见:
上面可以看到,对str对象使用运算符 + 实际上返回一个新str实例,从而保持调用实例 a 的值不被修改。要更改它, 我们需要显式地将新实例赋给a。
让我们实现在Order类中使用运算符向我们的购物车追加新项目的能力。我们将遵循建议的做法,并使运算符返回一个新的Order实例,它有我们所需的更改,而不是直接对我们的实例进行更改:
同样, 还有__sub__,__mul__等其他重定义-,*的特殊方法。这些方法也应返回类的新实例。
缩写:+=运算符
运算符+=是表达式obj1=obj1 + obj2的缩写。与之对应的特殊方法是__iadd__。__iadd__方法应直接对self参数进行更改,并返回结果 (可能也可能不是self)。此行为与后者创建新对象并返回的方式__add__完全不同,正如你在上面所看到的那样。
大致而言, 对两个对象使用+=都等同于:
这里,result是__iadd__返回的值。第二次分配由 Python 自动处理,这意味着你不需要显式分配obj1到结果,就像在obj1=obj1 + obj2中的情况一样。
让我们在Order类中实现,以便新的项目可以追加到购物车使用:
可以看到, 任何更改都是直接对self进行的,然后返回。返回一些随机值时会发生什么情况,如字符串或整数?
尽管相关项目被追加到购物车中,但order的值更改为了__iadd__所返回的值。Python 隐式地处理了分配的任务。如果忘记在实现中返回某些内容,这可能会导致令人惊讶的结果:
由于所有 Python 函数 (或方法) 都是隐式返回None的,因此order被重新分配了None,REPL 会话在order检查时不会显示任何输出。看order的类型,现在是NoneType。因此,请始终确保你在__iadd__实现中返回一些内容,并且它是运算的结果,而不是其他任何内容。
类似于__iadd__,你对定义-=,*=,/=有__isub__,imul,__idiv__等其他特殊的方法。
注:当你的类定义中缺失__iadd__等函数,但你仍对对象使用它们的运算符,Python 使用__add__等函数使用其运算符来获取操作的结果并将其分配给调用实例。一般而言,只要在你的类中__add__等函数正常工作 (返回某种操作的结果),不实现__iadd__等函数就是安全的。
Python文档对这些方法有很好的解释。另外,请看一下这个示例,它显示了使用+=等不可变类型时所涉及的警告和其他操作。
使用 对对象进行索引和切片
运算符称为索引运算符,用于 Python 中的各种情境文中,例如在序列中的索引处获取值、获取与字典中的键值关联的值或通过切片获取序列的一部分。可以使用特殊方法__getitem__更改其行为。
让我们配置我们的Order类,以便我们可以直接使用该对象并从购物车中获取项目:
你会注意到__getitem__的参数名称不是index而是key。这是因为该参数可以主要为三种形式:一个整数值,在这种情况下,它是一个索引或字典键值;一个字符串值,在这种情况下,它是一个字典键值;一个切片对象,在这种情况下,它将切片该类使用的序列。虽然还有其他可能性,但这些都是最常见的。
由于我们的内部数据结构是一个列表,我们可以使用运算符切片列表,就像在这种情况下,参数key将是切片对象。这是在你的类中定义__getitem__的最大优点之一。只要你使用支持切片的数据结构 (列表、元组、字符串等),就可以将对象配置为直接切片结构:
注:有一个类似的特殊方法__setitem__,用于定义obj[x]=y的行为。此方法除了self外接受两个参数 (通常称为key和value) ,还可用于将key对应值更改为value。
反向运算符: 使类在数学上正确
在定义__add__,sub,__mul__等类似的特殊方法时, 当类实例是左侧操作数时,可以使用运算符,如果类实例是右侧操作数,则运算符将无法工作:
如果你的类表示像向量、坐标或复数等数学实体,则应用运算符应在两种情况下都正常工作,因为它是有效的数学运算。
此外,如果运算符只在实例为左操作数时起作用,则在许多情况下,我们违反了交换律的基本原理。因此,为了帮助你使类在数学上正确,Python 提供了反向特殊方法,例如__radd__,rsub,__rmul__等等。
这些句柄调用 (如x + obj,x - obj和x * obj),其中x不是相关类的实例。就像__add__等函数一样,这些反向特殊方法应返回一个修改后的新的类实例,而不是修改调用实例本身。
让我们在Order类中古董配置__radd__,使其实现在购物车的前面追加一些东西的功能。当购物车按订单的优先级组织时,可以使用此方法:
要归纳所有的这些点,最好看看一个实现这些运算符的示例类。
让我们从实现我们自己的类CustomComplex来表示复数开始。我们的类对象将支持各种内置函数和运算符,使它们的行为与内置的复数类非常相似:
构造函数只处理一种调用CustomComplex(a, b)。它采用位置参数,表示复数的实部和虚部。
让我们在类内定义两种函数conjugate和argz,并分别给出复数共轭和复数的参数:
注:__class__不是特殊方法,而是默认情况下存在的类属性。它有一个对类的引用。通过在这里使用,我们得到了它,然后以通常的方式调用构造函数。换言之,这等同于CustomComplex(real, imag)。这样做是为了避免如果某天类的名称发生更改所要导致的重构代码。
接下来我们配置abs来返回复数的模量:
我们将按照建议的__repr__和__str__的区别,为解析字符串表示形式使用前者,为了更"漂亮" 的表示后者。
__repr__方法将简单地返回一个CustomComplex(a, b)字符串,以便我们可以调用eval重新创建对象,而__str__方法将返回括号中的复数,如(a+bj):
在数学上, 可以添加任意两个复数或将实数添加到复数中。让我们用这样的方式配置运算符+,这样它就可以在这两种情况下工作。
该方法将检查右侧运算符的类型。如果它是int或float,它将只增加实部 (因为任何实数a等价于a+0j),而在是另一个复数的情况下,它的两个部分都会更改:
同样,我们定义-和*的行为:
由于加法和乘法都是有交换律的,我们可以通过在__radd__和__rmul__中分别调用__add__和__mul__来定义它们的反向算子。另一方面,由于减法是不可交换的,__rsub__的行为需要被定义:
注:你可能已经注意到, 我们没有添加构造来处理CustomComplex实例。这是因为,在这种情况下,两个操作数都是我们类的实例,__rsub__不负责处理操作。相反,__sub__将被调用。这是一个微妙但重要的细节。
现在,我们来看看这两个运算符,==和!=。用于它们的特殊方法分别是__eq__和__ne__。如果两个复数的实部和虚部分别相等,则它们是相等的。若其中任一不等,则两个复数不等:
注:浮点数参考(floating-point-gui.de/errors/comparison/)是一篇讨论了如何比较浮点精度和浮点数的文章。它强调了直接比较浮点的注意事项,这正是我们在这里做的事情。
还可以使用简单的公式得到复数的幂。我们使用特殊方法__pow__为内置的pow和运算符**配置行为:
注:仔细查看方法的定义。我们调用abs是为了得到复数的模量。因此,一旦定义了类中特定函数或运算符的特殊方法,就可以在同一类的其他方法中使用它。
让我们创建这个类的两个实例,一个具有正虚部,一个具有负虚部:
字符串表示形式:
使用带repr的eval重新创建对象:
加法、减法和乘法:
相等不等检查:
最后得到复数的幂:
正如你所看到的, 我们的自定义类的对象的行为和外观类似于内置类,并且非常 Pythonic。此类的完整示例代码如下:
在本教程中,你了解了 Python 数据模型以及数据模型如何用于生成 Pythonic 的类。你了解了如何更改内置函数 (如len、abs、str、bool等等) 的行为。你还了解了如何更改内置运算符的行为(如+、-、*、**等等)。
读完本文后,你可以很好地创建类,利用 Python 的最佳惯用功能,使你的对象 Pythonic!