Python 里名字前后都是双下划线的方法(__init__、__repr__、__len__……)叫特殊方法,也叫魔术方法(magic methods)、dunder methods(double underscore 的缩写)。它们是让你自定义的类行为像内置类型的秘诀:让 len(obj)、obj[k]、for x in obj、obj1 + obj2、with 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)) print(str(p)) print(p)
print([p, p])
|
__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)
v1 = Version(1, 5) v2 = Version(2, 0) print(v1 < v2) print(v1 == v2)
|
懒办法:@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 == b → hash(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) print(v1 * 3)
|
对应方法:
__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)) print(d[0]) print(3 in d)
|
只要有 __len__ 和 __getitem__,Python 就把你的类当成序列——for x in obj、obj[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)
|
__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))
|
用来做”带状态的函数”——比闭包更结构化。装饰器有时也基于 __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 print(f.x) print(f.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])
|
判断 key 是 slice 还是 int,可以支持切片。
十三、比较相等的关键规则
a == b 的完整过程:
- 调
a.__eq__(b);
- 若返回
NotImplemented,调 b.__eq__(a);
- 若都返回
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
| """分数类,展示常用魔术方法。"""
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}") print(f"{a} - {b} = {a - b}") print(f"{a} * {b} = {a * b}") print(f"{a} / {b} = {a / b}") print(-a, abs(-a)) print(a > b) print(float(a)) print(bool(Fraction(0)))
|
十七、常见陷阱
- 重写
__eq__ 忘 __hash__:实例不能进 set / dict。
- 哈希用可变字段:改变字段后 set 里再也找不到。
__init__ 里 return 值:会 TypeError。
__enter__ 忘 return self:with X() as x: 里 x 是 None。
__getattr__ 与 __getattribute__ 混用:前者只在找不到时触发,后者每次访问都触发,容易死循环。
- 算术方法返回类型不一致:
Vector + Vector 应该返回 Vector,别返回 tuple。
十八、小结与延伸阅读
- 魔术方法让类”表现得像内置类型”;
- 至少写
__init__ 和 __repr__;
- 想比较写
__eq__ + total_ordering;
- 想可哈希写
__hash__;
- 想运算符重载写
__add__ 等;
- 想像容器写
__len__ / __getitem__;
- 想可迭代写
__iter__;
- 想
with 写 __enter__ / __exit__;
- 想可调用写
__call__。
延伸阅读:
下一篇 类方法、静态方法与 property 我们讲三种方法风格的区别与用法。