碧阑干外绣帘垂,
猩色屏风画折枝。
——韩翎《已凉》
本期文章8400字
根据之前文章的后台统计数据推算
本期预计所需阅读时间50分钟
本系列文章已加入“维权骑士”(rightknights.com)的版权保护计划
本文原创内容受到保护
魔术方法(Magic Methods)和操作符重载
魔术方法是一种特殊的方法,魔术方法的名字的开头和结尾都有两个下划线(__)。
说到两个下划线,很容易想到,到目前为止,我们所遇到的唯一一个魔术方法是__init__(详见上期文章),但还有其他好多个魔术方法。它们用于实现那些无法表示为普通方法的功能。
魔术方法的一个常见用途是运算符重载。
运算符重载的意思是,在某个特定的类里重新定义某个已有的运算符,以允许在这个类的对象上使用+和*等运算符。
来看例子,用到的魔术方法是"__add__",它重写了"+"运算符。
class Vector2D:
def__init__(self, x, y):
self.x = x
self.y = y
def__add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
first = Vector2D(5, 7)
second = Vector2D(3, 9)
result = first + second
print(result.x)
print(result.y)
运行结果:
可以看到,魔术方法__add__定义了这个类的对象的加法操作(即使用+运算符进行运算时的运算方式)。按常理来说,如果没有这个魔术方法,默认的+运算符是不能对Vector2D类的对象进行加法运算的,我们通过使用__add__魔术方法,定义出了特别针对于Vector2D类的加法运算,也就是上文说的“允许在这个类的对象上使用+运算符”。一旦定义了__add__方法,我们就可以使用+运算符运算这个类的任意两个对象。
也就是说,__add__方法允许为我们自己写的某个类的对象的+运算符定义自定义行为。在上面的例子中,__add__方法把+运算符左右两个对象的相应属性相加,并返回包含运算结果的新对象。
测试题17.1.
实例化出一个对象时,自动调用到的魔术方法是?
点击下方空白区域查看答案
▼
__init__
要明白的是,只要方法名字两端各有两个下划线符号,这个方法就是魔术方法。
所以我们可以想到,既然每个魔术方法都是针对某个特定的运算符来进行重新定义,那么魔术方法的总数量必然是有限的,而且每个运算符对应特定名称的一个魔术方法,魔术方法的名称与运算符的对应关系是Python预先规定好了的。
以下是一些常见运算符所对应的魔术方法:
比如,表达式
x + y #加法运算过程
在执行运算时实际上是被转换为
x .__ add __(y) #调用对象x的一个方法的过程
常见的数据类型都已经预先包含了__add__等方法——如整数的__add__方法的功能是把数值相加并返回相加之和,字符串的__add__方法的功能是把字符串拼接起来并返回拼接后的字符串,这些都是我们使用+等运算符对它们执行运算时已经见过好多次的了。
但是如果上面表达式中的x所对应的类不包含__add__方法,并且x和y的类型不同,则系统会自动调用y .__ radd __(x),也就是y的__radd__方法。由此可见,如果想使用+运算符进行计算,那么参与运算的两个对象要么是同类对象并且这个类包含__add__方法,要么不是同类对象并且某一个对象所对应的类中有__radd__方法。
对于刚刚提到的所有魔术方法,都有相对应的"r方法",这些"r方法"比原来的魔术方法的名字多了一个r(位于开头的双下划线符号之后),用以包括使用相应运算符对不同类的对象进行运算时,所要调用的代码。来看例子:
class SpecialString:
def__init__(self, cont):
self.cont = cont
def__truediv__(self, other):
line = "=" * len(other.cont)
return "\n".join([self.cont, line, other.cont])
spam = SpecialString("spam")
hello = SpecialString("Hello world!")
print(spam / hello)
运行结果:
>>>
spam
============
Hello world!
>>>
在上面的例子中,我们为我们的SpecialString类定义了除法运算。当然,具体这个“除法运算”做什么,是由我们的__truediv__魔术方法所定义的。__truediv__方法只是向/运算符定义了一些代码,具体这些代码的功能还是不是“除法”,就不一定了。正如在上面的例子中,我们定义了SpecialString类的除法运算,但是我们的这个“除法运算”只是在两个字符串中间插入一些等号,并没有执行任何常规意义上的“除法”操作。
我们来分析一下上面示例中的代码,以更好地理解魔术方法的调用过程:
我们实例化出了两个SpecialString类的对象,即spam和hello。
spam作为SpecialString类的一个实例,通过__init__方法把参数字符串"spam"赋予给了这个对象的cont属性。同样地,SpecialString类实例化出的hello对象把参数字符串"Hello world!"作为其cont属性。实际上,通过__init__方法中的代码我们可以看出,SpecialString类的任何一个实例对象都只有一个属性,即cont属性。
回想一下上面的内容:“/”运算符对应的魔术方法是__truediv__。另外,因为spam和hello是同一个类的两个对象,所以,Python在运行这份代码的时候,把“print(spam / hello)”这行语句转换为了下面的代码:
spam.__truediv__(hello)
这个时候请注意,“.__ truediv__”左边的东西(即spam)被视为主体,而右边的东西(即hello)则被视为参数。所以,我们可以理解为这里实际上调用的是spam对象的__truediv__方法,传入这个方法的参数是hello对象。
然后,看spam对象的__truediv__方法:self.cont就是spam.cont,即spam对象的cont属性,也就是字符串“spam”。 同样,other.cont就是hello.cont,即hello对象的cont属性,也就是字符串“Hello world!”。
因此,在执行这个__truediv__方法时,return语句实际上是:
return "\n".join([spam.cont, line, hello.cont])
len(other.cont)实际上是len("Hello world!"),也就是字符串“Hello world!”中的字符数,即12,因此,在两个字符串之间一共插入12个等号"=",最终实现了上文给出的运行结果。
测试题17.2.
如果A类没有包含任何魔术方法,那么,下面的表达式:
A( )^B( )
实际上执行的操作是?
点击下方空白区域查看答案
▼
B().__rxor__(A())
A不包含任何魔术方法,那么当然就不包含与^运算符相对应的魔术方法。所以这里调用到的是__xor__方法的"r方法",同时当然是B类的方法,所以总而言之这里调用到的是B类的__rxor__方法。
Python还提供了用于进行“比较”操作的魔术方法,如下:
注:
lt是lower than(小于)的缩写,
le是lower and equal(小于等于)的缩写,
eq是equal(等于)的缩写,
ne是not equal(不等于)的缩写,
gt是greater than(大于)的缩写,
gt是greater and equal(大于等于)的缩写。
上面各项用于“比较”的魔术方法,在常见类型(如整数和字符串)的定义中,返回值是布尔类型True或False,有关这个返回值,我们在之前的文章中说到判断运算符的时候(第4篇文章)提到过。
但是需要注意的是,与之前的魔术方法一样,在我们自己定义的类中,这些魔术方法不一定真的要实现“判断”的功能,这些魔术方法只是在运行到相应运算符的时候调用的方法而已,具体这些方法所实现的功能是什么,就由我们自己来定了。
当执行等于与不等的判断时,如果未定义__ne__(也就是未定义“不等”的判断方式),则把__eq__的相反判断方式视为“不等”的判断方式。
其他运算符之间没有这种关系。
来看例子:
class SpecialString:
def__init__(self, cont):
self.cont = cont
def__gt__(self, other):
for index in range(len(other.cont)+1):
result = other.cont[:index] + ">" + self.cont
result += ">" + other.cont[index:]
print(result)
spam = SpecialString("spam")
eggs = SpecialString("eggs")
spam > eggs
运行结果:
>>>
>spam>eggs
e>spam>ggs
eg>spam>gs
egg>spam>s
eggs>spam>
>>>
如上文所述,我们可以为重载的运算符定义任何自定义行为。比如在上面这个例子中,我们就并没有真的去判断是不是“spam大于eggs”,而是执行了一些其他的操作(不一步一步分析了,但是应该比较容易能看得懂)。
测试题17.3.
魔术方法__le__所对应的运算符是?
领取专属 10元无门槛券
私享最新 技术干货