21-Python类机制的深度解析: 全方位介绍Python中的魔法方法,一网打尽

楔子

下面我们来看一下Python中的魔法方法,我们知道Python将操作符都抽象成了一个魔法方法(magic method),实例对象进行操作时,实际上会调用魔法方法。也正因为如此,numpy才得以很好的实现。

那么Python中常见的魔法方法都有哪些呢?我们按照特征分成了几类,下面就来看看魔法方法都有哪些,然后再举例说明它们的用法。

魔法方法概览

我们根据不同的特征分为了以下几类:

注意:有的方法是Python2中的,但是在Python3中依然存在,但是不推荐使用了。比如:__cmp__、__coerce__等等,我们就没有画在图中。

img

下面我们就来介绍一下上面的那些魔法方法的实际用途。

魔法方法介绍

构建以及初始化

__new__和__init__我们之前已经见识过了,还有一个__del__是做什么 呢?我们一起来看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Girl:

def __new__(cls, *args):
print("__new__")
return object.__new__(cls)

def __init__(self):
print("__init__")

def __del__(self):
print("__del__")


girl = Girl()
print("################")
"""
__new__
__init__
################
__del__
"""

__del__被称为析构函数,当一个实例对象被销毁之后会调用该函数。如果没有销毁,那么程序结束时也会调用。

比较操作

Python的比较操作符也抽象成了魔法方法,a == b,等价于a.eq(b)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Girl:

def __init__(self, name):
self.name = name

def __eq__(self, other):
return "==", self.name, other.name

def __ne__(self, other):
return "!=", self.name, other.name

def __le__(self, other):
return "<=", self.name, other.name

def __lt__(self, other):
return "<", self.name, other.name

def __ge__(self, other):
return ">=", self.name, other.name

def __gt__(self, other):
return ">", self.name, other.name


girl1 = Girl("girl1")
girl2 = Girl("girl2")

print(girl1 == girl2) # ('==', 'girl1', 'girl2')
print(girl1 != girl2) # ('!=', 'girl1', 'girl2')
print(girl1 < girl2) # ('<', 'girl1', 'girl2')
print(girl2 <= girl1) # ('<=', 'girl2', 'girl1')
print(girl2 > girl1) # ('>', 'girl2', 'girl1')
print(girl2 >= girl1) # ('>=', 'girl2', 'girl1')

我们看到如果是a > b,那么会调用a的__gt__方法,self就是a、other就是b;如果是b > a,那么调用b的__gt__方法,self就是b、other就是a;也就是说谁在前面,就调用谁的魔法方法。

但如果a > b,并且type(a)内部没有定义__gt__呢?那么会尝试调用type(b)内部的__gt__,如果都没有定义,那么就会调用object的__gt__,显然这个时候就会报错了。

注意:如果操作符两边有一个是内置对象、或者内置对象的实例对象,那么会直接调用我们创建的实例对象的魔法方法(前提是定义了)。比如:123 != girl1,那么直接调用girl1的__ne__,尽管整数对象也有__ne__。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Girl:

def __init__(self, name):
self.name = name

def __eq__(self, other):
return self.name, other


girl = Girl("matsuri")
# 如果其中一方为内置,那么直接调用girl的__eq__
# 如果girl在左边就更不用说了
print(girl == 123) # ('matsuri', 123)
print(123 == girl) # ('matsuri', 123)
print(object == girl) # ('matsuri', <class 'object'>)
print(girl == object) # ('matsuri', <class 'object'>)

单目运算

下面再来看看单目运算,估计很多人都不一定能百分百说出对应魔法方法的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Girl:

# +self 的时候调用
def __pos__(self):
return "__pos__"

# -self 的时候调用
def __neg__(self):
return "__neg__"

# abs(self) 的时候会调用, 也可以是np.abs(self), 但不推荐numpy调用
def __abs__(self):
return "__abs__"

# ~self 的时候调用
def __invert__(self):
return "__invert__"

# round(self, n) 的时候调用
def __round__(self, n=None):
return f"__round__, {n}"

# math.floor(self)的时候调用, 也可以是np.floor(self), 但不推荐numpy调用
def __floor__(self):
return "__floor__"

# math.ceil(self)的时候调用, 也可以是np.ceil(self), 但不推荐numpy调用
def __ceil__(self):
return "__ceil__"

# math.trunc(self)的时候调用, 也可以是np.trunc(self), 或者int(self)
# 但不推荐numpy调用
def __trunc__(self):
return "__trunc__"


girl = Girl()
import numpy as np
import math


# 1. +girl触发__pos__
print(+girl) # __pos__

# 2. -girl触发__pos__
print(-girl) # __neg__
"""
注意: 不可以写成 0 + girl 和 0 - girl, 尽管我们知道在数学上这与 girl和-girl是等价的
但是在Python中不行, 因为这样会调用girl的__radd__和__rsub__, 我们后面会说
"""

# 3. abs(girl)或者np.abs(girl)触发__abs__
print(abs(girl)) # __abs__
print(np.abs(girl)) # __abs__

# 4. ~girl触发__invert__
print(~girl) # __invert__

# 5. round(girl)触发__round__
print(round(girl)) # __round__, None
print(round(girl, 2)) # __round__, 2

# 6. math.floor(girl), np.floor(girl)触发__round__
print(math.floor(girl)) # __floor__
print(np.floor(girl)) # __floor__

# 7. math.ceil(girl), np.ceil(girl)触发__round__
print(math.ceil(girl)) # __ceil__
print(np.ceil(girl)) # __ceil__

# 8. math.trunc(girl), np.trunc(girl)触发__trunc__
print(math.trunc(girl)) # __trunc__
print(np.trunc(girl)) # __trunc__
# __trunc__表示截断, 只保留整数位, 所以int(girl)也是可以触发的
# 但如果是int(girl)这种方式, 它要求__trunc__必须返回一个整数
try:
int(girl)
except Exception as e:
print(e) # __trunc__ returned non-Integral (type str)
Girl.__trunc__ = lambda self: 666
print(int(Girl())) # 666

以上便是单目运算的一些魔法方法,但是说实话个人觉得只有__pos__、__neg__、__invert__会用上,因为我们可能希望一些操作的调用方式尽可能简单,所以会通过重些+、-、~ 操作符对应的魔法方法,来赋予实例对象一些特殊的含义。

至于其它的简单了解一下即可,不过注意的是,有些方法numpy也是可以是使用的,但是并不推荐。

算术运算

算术运算是比较常用的了,我们来看看算数运算对应的魔法方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class Girl:

# a + b 的时候调用, self就是a、other就是b
def __add__(self, other):
return "__add__"

# a - b 的时候调用, self就是a、other就是b
def __sub__(self, other):
return "__sub__"

# a * b 的时候调用, self就是a、other就是b
def __mul__(self, other):
return "__mul__"

# a // b 的时候调用, self就是a、other就是b
def __floordiv__(self, other):
return "__floordiv__"

# a / b 的时候调用, self就是a、other就是b
# 还有一个__div__
def __truediv__(self, other):
return "__truediv__"

# a + b 的时候调用, self就是a、other就是b
def __mod__(self, other):
return "__mod__"

# divmod(a, b) 的时候调用, self就是a、other就是b
def __divmod__(self, other):
return "__divmod__"

# a ** b 的时候调用, self就是a、other就是b
def __pow__(self, power, modulo=None):
return "__pow__"

# a << b 的时候调用, self就是a、other就是b
def __lshift__(self, other):
return "__lshift__"

# a >> b 的时候调用, self就是a、other就是b
def __rshift__(self, other):
return "__rshift__"

# a & b 的时候调用, self就是a、other就是b
def __and__(self, other):
return "__and__"

# a | b 的时候调用, self就是a、other就是b
def __or__(self, other):
return "__or__"

# a ^ b 的时候调用, self就是a、other就是b
def __xor__(self, other):
return "__xor__"

# a @ b 的时候调用, self就是a、other就是b
def __matmul__(self, other):
# 这个方法是用在矩阵运算的, Python在3.5版本的时候将@抽象成了这个方法
# 比如numpy的两个数组如果想进行矩阵之间的相乘
# 除了np.dot(arr1, arr2)之外, 还可以直接arr1 @ arr2
return "__matmul__"


girl1 = Girl()
girl2 = Girl()

print(girl1 + girl2) # __add__
print(girl1 - girl2) # __sub__
print(girl1 * girl2) # __mul__
print(girl1 // girl2) # __floordiv__
print(girl1 / girl2) # __truediv__
print(girl1 % girl2) # __mod__
print(divmod(girl1, girl2)) # __divmod__
print(girl1 ** girl2) # __pow__
print(girl1 << girl2) # __lshift__
print(girl1 >> girl2) # __rshift__
print(girl1 & girl2) # __and__
print(girl1 | girl2) # __or__
print(girl1 ^ girl2) # __xor__

常见的算术运算大概就是上面这些,还是很简单的。

反射算术运算

反射算术运算指的是什么呢?比如: a + b,我们知道会调用a的__add__,但如果type(a)中没有定义__add__,那么会尝试寻找b的__radd__。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class A:

def __add__(self, other):
return "class A:", type(self).__name__, type(other).__name__


class B:

def __radd__(self, other):
return "class B:", type(self).__name__, type(other).__name__


a = A()
b = B()

# type(a)中定义了__add__, 那么优先调用
print(a + b) # ('class A:', 'A', 'B')

# 如果type(a)中没有定义__add__, 那么会去看type(b)中有没有定义__radd__
del A.__add__
print(a + b) # ('class B:', 'B', 'A')


# 如果a + b, 其中一个是内置对象, 那么做法和比较操作是类似的
"""
如果是一方为内置对象, 比如:
a + 123: 直接调用a的__add__
123 + a: 直接调用a的__radd__
"""
print(123 + b) # ('class B:', 'B', 'int')

try:
123 + a
except Exception as e:
# 显然a没有__radd__, 因此会选择object的__add__, 显然这个时候报错了
print(e) # unsupported operand type(s) for +: 'int' and 'A'

# 但a是有__add__的, 所以直接走a的__add__
A.__add__ = lambda self, other: (self, other)
print(a + "xxx") # (<__main__.A object at 0x0000020FB72A82B0>, 'xxx')

其它操作符也是类似的,a 操作符 b会调用a的__xxx__,但如果a没有,会尝试搜寻b的__rxxx__

赋值算术运算

赋值算术运算适用于类似于+=这种形式,比如:

1
2
3
4
5
6
7
8
9
10
class A:

def __iadd__(self, other):
return type(self).__name__ + other


a = A()
# 会调用__iadd__, 参数self就是a, other就是">>>"
a += ">>>"
print(a) # A>>>

比较简单,其它的也与此类似。

序列操作

下面我们看看序列操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A:

def __len__(self):
return 123


a = A()
print(len(a)) # 123

# 所以len(a)本质上会调用type(a).__len__(a)

# 注意: 是type(a).__len__(a), 不是a.__len__()
a.__len__ = "xxx"
print(a.__len__) # xxx
print(len(a)) # 123


# 注意: __len__必须返回一个整型, 否则报错

此外,__len__还有充当布尔值的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:
pass


# 默认返回的是True
print(bool(A())) # True

A.__len__ = lambda self: 0
print(bool(A())) # False


# __len__返回的是0, 为假, 所以结果为False
# 当然真正起到决定性作用的是__bool__方法, 如果定义了__bool__, 那么以__bool__的返回值为准,必须返回布尔类型的值
# 没有__bool__, 那么解释器会退化, 寻找__len__
A.__bool__ = lambda self: True
print(bool(A())) # True

所以解释器具有退化功能,会优先寻找某个方法,但如果没有,那么会退化寻找替代方法。在后面,我们还会看到类似的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A:

def __getitem__(self, item):
print(item)

def __setitem__(self, key, value):
print(key, value)

def __delitem__(self, key):
print(key)


# 上面三个可以让我像操作字典一样, 操作实例对象
a = A()
a["xxx"] # xxx
a["xxx"] = "yyy" # xxx yyy
del a["aaa"] # aaa

# 不仅如此, 它们还可以作用于切片
a[3: 4] # slice(3, 4, None)
a["你好": "我很可爱": "请亏我全"] # slice('你好', '我很可爱', '请亏我全')
a["你好": "我很可爱": "请亏我全"] = "屑女仆" # slice('你好', '我很可爱', '请亏我全') 屑女仆
del a["神乐mea": "迷迭迷迭帕里桑"] # slice('神乐mea', '迷迭迷迭帕里桑', None)

这里我们再着重说一下__getitem__,我们说Python的for循环本质上会调用内部的__iter__,但如果内部没有定义,那么解释器会退化寻找__getitem__。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class A:

def __getitem__(self, item):
return item


lst = []
for idx in A():
if idx > 5:
break
lst.append(idx)

# 我们看到遍历A()的时候, 在没有__iter__的时候会去找__getitem__
# 并且默认传递0 1 2 3......, 所以循环遍历的话默认是无休止的
print(lst) # [0, 1, 2, 3, 4, 5]


class B:

def __init__(self):
self.lst = ["古明地觉", "芙兰朵露", "雾雨魔理沙", "八意永琳", "琪露诺"]
self.__len = len(self.lst)

def __getitem__(self, item):
if item == self.__len:
raise StopIteration
return self.lst[item]


print(list(B())) # ['古明地觉', '芙兰朵露', '雾雨魔理沙', '八意永琳', '琪露诺']
(lst := []).extend(B())
print(lst) # ['古明地觉', '芙兰朵露', '雾雨魔理沙', '八意永琳', '琪露诺']

怎么样,是不是很神奇呢?当然for循环肯定是优先寻找__iter__,没有的话会进行退化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:

def __reversed__(self):
return "__reversed__"

def __contains__(self, item):
return item


print(reversed(A())) # __reversed__

# a in b等价于 b.__contains__(a), 但是会自动将返回值变成bool值
# 也就是说我们上面的return item其实等价于return bool(item)
print("xx" in A()) # True
print("" in A()) # False
print([] in A()) # False

最后一个__missing__比较特殊,它是针对于字典的,我们来看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class A(dict):

def __missing__(self, key):
return str(key).upper()


a = A({"name": "夏色祭", "age": -1})
print(a["name"]) # 夏色祭
print(a["Name"]) # NAME

# 当我们使用获取元素时, 首先调用__getitem__
# 由于我们没有重写, 显然调用父类的__getitem__, 如果获取到结果, 那么直接返回
# 获取不到, 那么会调用__missing__, 如果没有重写则报错, 重写的话则是__missing__的返回值


# 所以我们可以这么做
class MyDict(dict):

def __getitem__(self, item):
return super().__getitem__(item)

def __missing__(self, key):
return f"{key!r}不存在"


d = MyDict({"name": "夏色祭", "age": -1})
print(d["age"]) # -1
print(d["AGE"]) # 'AGE'不存在
# 首先会执行我们重写的__getitem__, 但是我们通过super().__getitem__(item), 通过父类来获取对应的value
# 父类发现在获取不到的时候, 会去找__missing__, 如果我们定义了就走我们重写的__missing__
# 没有重写, 对于父类而言则报错, 因为dict没有__missing__

类型转换

很简单的内容了,我们直接来看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class A:

def __int__(self):
return 123

def __index__(self):
return 789

# 上面两个作用类似, 在执行int(self)时候所调用
# 但是存在一个优先级

# 默认是__int__
print(int(A())) # 123

# 如果没有__init__, 执行__index__
del A.__int__
print(int(A())) # 789
# __init__和__index__要求必须返回整型


class B:

# 必须返回浮点型
def __float__(self):
return 3. # 3.是可以的, 但是3不行

print(float(B())) # 3.0


class C:
# 针对复数
def __complex__(self):
return 1 + 3j

print(complex(C())) # (1+3j)

上下文管理

这部分不说了,可以看我的这一篇博客:https://www.cnblogs.com/traditional/p/11487979.html,通过源码分析contextlib标准库介绍with语句。

属性访问

__getattr__、__setattr__、__delattr__和我们之前说的__getitem__、__setitem__、__delitem__类似,只不过这里是通过.的方式来访问的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:

def __getattr__(self, item):
print(item)

def __setattr__(self, key, value):
print(key, value)

def __delattr__(self, item):
print(item)


a = A()
a.name # name
a.name = "夏色祭" # name 夏色祭
del a.age # age

getattr、setattr、delattr这几个内置函数本质上也是调用这几个魔法方法,只不过它额外做了一些其它的工作。以getattr为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class A:

def __init__(self):
self.name = "夏色祭"
self.age = -1


print(getattr(A(), "name", "不存在的属性")) # 夏色祭
print(getattr(A(), "gender", "不存在的属性")) # 不存在的属性


# 指定了不存在的属性, 会返回默认值, 注意: getattr必须指定三个参数
# 否则属性不存在会报错, 而不是我们认为的None
# 可能有人觉得第三个参数不传就是None, 其实不是的


class B:

def __init__(self):
self.name = "夏色祭"
self.age = -1

def __getattr__(self, item):
try:
return self.__dict__[item]
except KeyError:
raise AttributeError


print(getattr(B(), "NAME", "不存在的属性")) # 不存在的属性
# 我们重写了__getattr__, 那么会调用我们重写的__getattr__
# 然后通过字典返回, 但是注意: 在__getattr__里面可千万不能通过.来访问一个不存在的属性
# 那样会陷入无限递归
# 如果存在的话, 直接返回; 但如果不存在, 一定要raise AttributeError, 这样的话才会返回getattr的第三个参数, 即默认值
# 如果是其它错误, getattr是无法捕获的; 正如自定义迭代器要raise StopIteration一样, 只有这样for循环才会捕捉到并终止迭代

对象调用

这一点我们好像很早之前就说过了,一个对象能否被调用,取决于它的类对象中是否定义了__call__。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Deco:

def __init__(self, func):
self.func = func

def __call__(self, *args, **kwargs):
print("start")
res = self.func(*args, **kwargs)
print("end")
return res


@Deco
def foo(name, age):
print(name, age)


foo("夏色祭", -1)
"""
start
夏色祭 -1
end
"""

小结

剩下的内容比较简单,当然描述符我们之前就说过了。最主要的是魔法方法的话,可以自己试一下就知道它们是干什么的了,没太大难度,这里就不说了。