特殊方法(魔术方法)详解

Python 里名字前后都是双下划线的方法(__init____repr____len__……)叫特殊方法,也叫魔术方法(magic methods)dunder methods(double underscore 的缩写)。它们是让你自定义的类行为像内置类型的秘诀:让 len(obj)obj[k]for x in objobj1 + obj2with obj:str(obj) 都能正常工作。这一篇把最常用的 20 多个魔术方法一次讲清。

一、初始化与析构

1
2
3
4
5
6
class Foo:
def __init__(self, x): # 初始化
self.x = x

def __del__(self): # 垃圾回收时调用(一般不用)
print(f"删除 {self}")
  • __init__:实例创建后调用;
  • __new__:创建实例本身(很少重写);
  • __del__:不保证被调用,别用来关闭资源。

二、字符串表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point:
def __init__(self, x, y):
self.x, self.y = x, y

def __repr__(self):
return f"Point({self.x}, {self.y})"

def __str__(self):
return f"({self.x}, {self.y})"


p = Point(3, 4)
print(repr(p)) # Point(3, 4) 调试用
print(str(p)) # (3, 4) 用户看
print(p) # (3, 4) print 走 __str__

# 在容器里
print([p, p]) # [Point(3, 4), Point(3, 4)] 容器打印走 __repr__
  • __repr__必写,给开发者看,最好是”能复制回来重建对象”的字符串;
  • __str__:可选,给用户看,不写就 fallback 到 __repr__

三、比较运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Version:
def __init__(self, major, minor):
self.major, self.minor = major, minor

def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)

def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)

def __le__(self, other):
return (self.major, self.minor) <= (other.major, other.minor)

# __gt__ / __ge__ / __ne__ 可以补齐或让 Python 反推


v1 = Version(1, 5)
v2 = Version(2, 0)
print(v1 < v2) # True
print(v1 == v2) # False

懒办法@functools.total_ordering 自动生成所有比较:

1
2
3
4
5
6
7
8
9
10
11
12
from functools import total_ordering

@total_ordering
class Version:
def __init__(self, major, minor):
self.major, self.minor = major, minor

def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)

def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)

只写 __eq__ 和一个顺序方法,其余自动生成。

四、哈希与不可变

1
2
3
4
5
6
7
8
9
class Point:
def __init__(self, x, y):
self.x, self.y = x, y

def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)

def __hash__(self):
return hash((self.x, self.y))

规则:

  • 一旦重写 __eq__,Python 会自动把 __hash__ 设为 None(不可哈希);
  • 想让实例能进 set / 做 dict key,就要显式定义 __hash__
  • __hash__ 必须与 __eq__ 一致:a == bhash(a) == hash(b)
  • 哈希基于不可变字段——可变字段变了,哈希失效。

五、算术运算

让实例支持 + - * / // 等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y

def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)

def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)

def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

def __repr__(self):
return f"Vector({self.x}, {self.y})"


v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(3, 6)

对应方法:

  • __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__
  • __iadd__, __isub__(就地版 +=
  • __radd__(当 x + self 时 x 不知道怎么处理,就调 self.__radd__(x)

六、容器协议

让实例像容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Deck:
def __init__(self):
self.cards = list(range(1, 53))

def __len__(self):
return len(self.cards)

def __getitem__(self, i):
return self.cards[i]

def __setitem__(self, i, v):
self.cards[i] = v

def __delitem__(self, i):
del self.cards[i]

def __contains__(self, x):
return x in self.cards


d = Deck()
print(len(d)) # 52
print(d[0]) # 1
print(3 in d) # True

只要有 __len____getitem__,Python 就把你的类当成序列——for x in objobj[i:j] 都能用。

七、迭代协议

想让实例可 for x in obj

1
2
3
4
5
6
7
8
9
10
11
12
13
class Countdown:
def __init__(self, start):
self.start = start

def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1

for x in Countdown(3):
print(x)
# 3 2 1

__iter__ 返回一个迭代器(或用 yield 让函数变生成器)。详细见 迭代器与可迭代对象

八、布尔与真值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Cart:
def __init__(self):
self.items = []

def __bool__(self):
return bool(self.items)

def __len__(self):
return len(self.items)


c = Cart()
if not c: # 空购物车为假
print("购物车空")

规则:Python 判真值时先看 __bool__,没有再看 __len__(返回 0 视为假),都没有默认为真。

九、上下文管理器 with

1
2
3
4
5
6
7
8
9
10
11
12
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self

def __exit__(self, exc_type, exc_val, tb):
import time
print(f"耗时 {time.time() - self.start:.3f}s")

with Timer() as t:
sum(i*i for i in range(1_000_000))
  • __enter__ 返回值绑给 as 后的名字;
  • __exit__ 无论有没有异常都会被调用;
  • 返回 True 会吞掉异常

详细见 with 语句与上下文管理器

十、可调用对象 __call__

让实例像函数一样被调用:

1
2
3
4
5
6
7
8
9
10
class Multiplier:
def __init__(self, factor):
self.factor = factor

def __call__(self, x):
return x * self.factor


double = Multiplier(2)
print(double(5)) # 10

用来做”带状态的函数”——比闭包更结构化。装饰器有时也基于 __call__ 写成类。

十一、属性访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo:
def __getattr__(self, name):
"""访问不存在的属性时调用。"""
return f"没有 {name}"

def __setattr__(self, name, value):
"""设置任意属性都会经过这里。"""
print(f"设置 {name}={value}")
super().__setattr__(name, value)

def __delattr__(self, name):
pass


f = Foo()
f.x = 10 # 设置 x=10
print(f.x) # 10
print(f.abc) # 没有 abc

__getattr__ 只在找不到属性时才被调用(不是每次访问都触发)。想每次都拦截用 __getattribute__(很少用)。

十二、上下文长度与切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyList:
def __init__(self, data):
self.data = data

def __getitem__(self, key):
if isinstance(key, slice):
return MyList(self.data[key])
return self.data[key]

def __repr__(self):
return f"MyList({self.data})"


m = MyList([1, 2, 3, 4, 5])
print(m[1:3]) # MyList([2, 3])

判断 keyslice 还是 int,可以支持切片。

十三、比较相等的关键规则

a == b 的完整过程:

  1. a.__eq__(b)
  2. 若返回 NotImplemented,调 b.__eq__(a)
  3. 若都返回 NotImplemented,用身份判断(a is b)。

写自定义比较时对”不同类型”返回 NotImplemented 而不是 False:

1
2
3
4
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)

十四、__slots__(回顾)

前面 属性、方法与 self 讲过:

1
2
class Point:
__slots__ = ("x", "y")

省内存、防拼错。

十五、其他常见魔术方法

方法 触发
__format__ format(x) / f-string
__bytes__ bytes(x)
__index__ 整数化(切片 index 等)
__reversed__ reversed(x)
__abs__ abs(x)
__int__ __float__ int(x) / float(x)
__copy__ __deepcopy__ copy 模块调用
__enter__ __exit__ with 语句
__aenter__ __aexit__ async with
__aiter__ __anext__ async for

十六、一个完整例子:分数类

综合展示多种魔术方法:

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
# fraction.py
"""分数类,展示常用魔术方法。"""

from math import gcd
from functools import total_ordering


@total_ordering
class Fraction:
def __init__(self, numer: int, denom: int = 1):
if denom == 0:
raise ZeroDivisionError("分母不能为 0")
# 约分
g = gcd(abs(numer), abs(denom))
sign = -1 if (numer < 0) ^ (denom < 0) else 1
self.numer = sign * abs(numer) // g
self.denom = abs(denom) // g

def __repr__(self):
return f"Fraction({self.numer}, {self.denom})"

def __str__(self):
return f"{self.numer}/{self.denom}" if self.denom != 1 else str(self.numer)

def __eq__(self, other):
if not isinstance(other, Fraction):
return NotImplemented
return self.numer * other.denom == other.numer * self.denom

def __lt__(self, other):
return self.numer * other.denom < other.numer * self.denom

def __hash__(self):
return hash((self.numer, self.denom))

def __add__(self, other):
return Fraction(
self.numer * other.denom + other.numer * self.denom,
self.denom * other.denom
)

def __sub__(self, other):
return Fraction(
self.numer * other.denom - other.numer * self.denom,
self.denom * other.denom
)

def __mul__(self, other):
return Fraction(self.numer * other.numer, self.denom * other.denom)

def __truediv__(self, other):
return Fraction(self.numer * other.denom, self.denom * other.numer)

def __neg__(self):
return Fraction(-self.numer, self.denom)

def __abs__(self):
return Fraction(abs(self.numer), self.denom)

def __float__(self):
return self.numer / self.denom

def __bool__(self):
return self.numer != 0


if __name__ == "__main__":
a = Fraction(1, 2)
b = Fraction(1, 3)
print(f"{a} + {b} = {a + b}") # 1/2 + 1/3 = 5/6
print(f"{a} - {b} = {a - b}") # 1/6
print(f"{a} * {b} = {a * b}") # 1/6
print(f"{a} / {b} = {a / b}") # 3/2
print(-a, abs(-a))
print(a > b) # True
print(float(a)) # 0.5
print(bool(Fraction(0))) # False

十七、常见陷阱

  1. 重写 __eq____hash__:实例不能进 set / dict。
  2. 哈希用可变字段:改变字段后 set 里再也找不到。
  3. __init__ 里 return 值:会 TypeError。
  4. __enter__ 忘 return selfwith X() as x: 里 x 是 None。
  5. __getattr____getattribute__ 混用:前者只在找不到时触发,后者每次访问都触发,容易死循环。
  6. 算术方法返回类型不一致Vector + Vector 应该返回 Vector,别返回 tuple。

十八、小结与延伸阅读

  • 魔术方法让类”表现得像内置类型”;
  • 至少写 __init____repr__
  • 想比较写 __eq__ + total_ordering
  • 想可哈希写 __hash__
  • 想运算符重载写 __add__ 等;
  • 想像容器写 __len__ / __getitem__
  • 想可迭代写 __iter__
  • with__enter__ / __exit__
  • 想可调用写 __call__

延伸阅读:

下一篇 类方法、静态方法与 property 我们讲三种方法风格的区别与用法。