关键词: Python 魔法方法 操作符重载
0.前言
在阅读Python的代码时,经常会看到以双下划线__
包裹起来的方法,最常见的就是在Python的类里的__init__
方法。这些方法被称之为魔法方法(Magic Method)。网上看了很多介绍Python魔法方法的文章,有的写得太过啰嗦,有的写得太过深奥。我个人觉得这篇文章写得非常简洁明了,于是将之翻译,稍微对原文内容做了一些修改,算是Python魔法方法相关知识的总结和归档。
1.介绍
所谓的Python魔法方法与真正的魔法并没有关系。这种方法的语法很令人费解,如:在方法名的开始和结尾都要加上两道下划线。你也很难探讨它们,如何口头表达一个名字为__init__
的方法?可能最好的口头表达方法就是”前后双下划init方法”,就是这样听起来也很难受。
那么关于__init__
方法有何神奇之处?答案就是,你不用直接调用它,调用的实现机制是在后台完成。当你用语句X = A()
创建一个A类的x实例时,Python会自动调用__new__
和__init__
方法。
关于操作符重载,我们可以使用+
来进行加法运算、合并字符串或列表。
1 | 4 + 5 |
我们甚至可以在自己的类中重载+
操作符乃至其它操作符。想要实现上述功能,我们就需要理解操作符重载的背后机制。每一个操作符号都有一个魔法方法(也称特殊方法)。+
号的魔法方法是__add__
方法,-
号的魔法方法是__sub__
方法。下面第2节表格显示的是全部操作符的魔法方法。
操作符重载的工作机制如下:如果我们有一个x + y
的表达式,x是类K的实例,那么Python将会检查类K的定义。如果K有__add__
方法,那么x + y
将会调用x.__add__(y)
,否则我们将会得到一个错误信息。
1 | class K(): |
2. 魔法方法总览
二进制操作符
操作符 | 方法 |
---|---|
+ |
object.__add__(self, other) |
- |
object.__sub__(self, other) |
* |
object.__mul__(self, other) |
// |
object.__floordiv__(self, other) |
/ |
object.__truediv__(self, other) |
% |
object.__mod__(self, other) |
** |
object.__pow__(self, other[,modulo]) |
<< |
object.__lshift__(self, other) |
>> |
object.__rshift__(self, other) |
& |
object.__and__(self, other) |
^ |
object.__xor__(self, other) |
| | object.__or__(self, other) |
增强赋值操作符
操作符 | 方法 |
---|---|
+= |
object.__iadd__(self, other) |
-= |
object.__isub__(self, other) |
*= |
object.__imul__(self, other) |
/= |
object.__idiv__(self, other) |
//= |
object.__ifloordiv__(self, other) |
%= |
object.__imod__(self, other) |
**= |
object.__ipow__(self, other[,modulo]) |
<<= |
object.__lshift__(self, other) |
>>= |
object.__rshift__(self, other) |
&= |
object.__iand__(self, other) |
| = | object.__ior__(self, other) |
注:在Markdown表格里输入|
会出现很多问题,上述表格中的|=
的中间并没有空格。
一元操作符
操作符 | 方法 |
---|---|
- |
object.__neg__(self) |
+ |
object.__pos__(self) |
abs() |
object.__abs__(self) |
~ |
object.__invert__(self) |
complex() |
object.__complex__(self) |
int() |
object.__int__(self) |
long() |
object.__long__(self) |
float() |
object.__float__(self) |
oct() |
object.__oct__(self) |
hex() |
object.__hex__(self) |
比较操作符
操作符 | 方法 |
---|---|
< |
object.__lt__(self, other) |
<= |
object.__le__(self, other) |
== |
object.__eq__(self, other) |
!= |
object.__ne__(self, other) |
>= |
object.__ge__(self, other) |
> |
object.__gt__(self, other) |
3. 例程:Length类
在接下来的Length
类里,我们将展示如何在自己的类中重载+
操作符。要完成上述功能,我们要重载__add__
方法。我们的类中也同时包含__str__
和__repr__
方法。Length
类的实例也包含长度或距离信息。一个实例的属性包含self.value
和self.unit
。
Length
类允许我们计算混合单元的表达式,如下例所示:
2.56 m + 3 yd + 7.8 in + 7.03 cm
Length
类可以如下所示的代码所使用:
1 | from unit_conversions import Length |
Length
类的代码:
1 | class Length(): |
注:补充关于Python中的前缀为单下划线、结尾为单下划线、前缀为双下划线、前缀和结尾为双下划线的各自含义,参照stackoverflow上的解答。
- _single_leading_underscore: 以单下划线作为前缀的变量或者函数被默认当作是内部变量和函数。如果使用
from a_module import *
导入时,这部分变量和函数不会被导入。不过如果使用import a_module
导入模块,仍可通过a_module._some_var
进行访问。 - single_trailingunderscore:避免与Python关键字冲突的一种习惯写法,举例:
Tkinter.Toplevel(master, class_='ClassName')
。 - __double_leading_underscore:以双下划线作前缀的名称对解释器有特定含义。Python会改写这些名称,以免与子类中定义的名称产生冲突。任何
__method
这种形式的标识符,都会在文本上被替换成_classname__method
,其中classname
是当前类名,并带上一个下划线作为前缀,具体理解可参考例子 。 - double_leading_and_trailing_underscore:Python的魔法方法,但也是一种惯例,确保Python系统中的名称不会和用户自定义的名称发生冲突的方式。
如果我们运行程序,将会得到以下输出结果:
1 | 4 |
我们可以使用__iadd__
方法来实现更多的功能:
1 | def __iadd__(self, other): |
现在我们可以写下以下程序:
1 | x += Length(1) |
在上述的例子当中,我们通过语句x += Length(1)
来实现加上1米的功能。可以肯定地说,你一定会同意x += 1
这种写法更加方便。对于Length(5, "yd") + 4.8
这类的表达式,我们也想实现类似的功能。如果有人使用整型数或者浮点型数,我们的Length
类将会自动将其转换为单位是”metre”的Length
对象。使用__add__
和__iadd__
方法来完成上述功能很简单,我们只需要检查参数”other”的类型即可。
1 | def __add__(self, other): |
如果一个人从右边加上整数或者浮点数,他也很有可能从左边执行相同的操作。让我们尝试一下:
1 | from unit_conversions import Length |
当然,表达式的左边必须是”Length”类,否则Python将会试图从int
类型里使用__add__
方法,这样就无法将Length
对象作为第二个参数进行处理。
对于此类问题Python也提供了一套解决方案。这就是__radd__
方法。它的工作原理是这样的:Python试图求得表达式5 + Length(3, "yd")
的值。首先表达式将调用int.__add__(5, Length(3, 'yd'))
,从而引发一个异常。然后表达式将试图调用Length.__radd__(Length(3, "yd"), 5)
。很容易想象出__radd__
的实现同__add__
很类似。
1 | def __radd__(self, other): |
更为明智的做法是在__radd__
方法中使用__add__
方法:
1 | def __radd__(self, other): |
下面的图标解释了方法__add__
和__radd__
之间的关系:
4. 标准类作为基类
我们有可能使用诸如int
,float
,dict
或者是list
的基类作为标准类。
我们将list
列表类型新填一个push
方法:
1 | class Plist(list): |
5. 练习
问题:写一个类似于之前定义的Length
类的名称为Ccy
的类。Ccy
类应该包含不同货币的值,诸如欧元、英镑、美元等。一个实例应该包含金额数量和金额单位。设计的类必须能够通过下列程序描述:
1 | from currencies import Ccy |
注:原文数据有误。
我的解决方法:
1 | class Ccy: |
参考链接: