1. 引言:什么是魔法方法
在 Python 中,以双下划线开头和结尾的方法(如 __init__、__str__)被称为魔法方法 (Magic Methods),也叫双下方法(Dunder Methods,Double Underscore 的缩写)。
它们不是给开发者直接调用的,而是由 Python 解释器在特定场景下自动触发。比如:
- 用
len(obj)时,解释器会去找obj.__len__(); - 用
obj[key]时,会去找obj.__getitem__(key); - 用
a + b时,会去找a.__add__(b)。
魔法方法的价值在于:让你的自定义类像内置类型一样自然地融入 Python 的语法体系 。一个设计良好的类,使用者不需要记住一堆 get_xxx()、set_xxx() 方法,而是可以直接用 len()、for 循环、==、+ 等原生语法操作它------这就是所谓的 Pythonic。
本文将从最常用的几个魔法方法讲起,逐步深入到运算符重载,最后手写一个完整的自定义集合类,把前面所有知识点串起来。
下面这张图概括了本文涉及的魔法方法分类,方便你建立整体认知:
2. 对象的生命周期与表示:__init__、__str__、__repr__
2.1 __init__:初始化对象
__init__ 是最常见的魔法方法,在对象创建后负责初始化实例属性。
python
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.x, p.y) # 1 2
需要注意 __init__ 与 __new__ 的区别:__new__ 负责创建 实例(返回实例对象),__init__ 负责初始化 实例(返回 None)。绝大多数场景只需要重写 __init__。
对象从创建到初始化的完整流程如下:
2.2 __str__:面向用户的字符串表示
__str__ 在调用 str(obj) 或 print(obj) 时被触发,目标是给人类阅读,应当简洁、可读。
python
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
print(Point(1, 2)) # (1, 2)
2.3 __repr__:面向开发者的字符串表示
__repr__ 在调用 repr(obj)、在交互式解释器中直接输入对象、以及对象被放进容器(如列表)打印时被触发。它的目标是尽可能精确地还原对象 ,理想情况下 eval(repr(obj)) 能重建一个等价对象。
python
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
p = Point(1, 2)
print(repr(p)) # Point(1, 2)
print([p, p]) # [Point(1, 2), Point(1, 2)]
2.4 最佳实践:只写 __repr__ 就够了
如果只定义了 __repr__ 而没有定义 __str__,那么 str() 会自动回退到 __repr__。因此一个常见的做法是:优先实现 __repr__,需要更友好的展示时再补充 __str__。
python
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
def __str__(self):
return f"({self.x}, {self.y})"
p = Point(1, 2)
print(str(p)) # (1, 2)
print(repr(p)) # Point(1, 2)
3. 容器协议:__len__ 与 __getitem__
3.1 __len__:支持 len()
实现 __len__ 后,对象就能被 len() 调用,同时也会影响布尔判断(bool(obj) 在 __len__ 返回 0 时为 False)。
python
class Playlist:
def __init__(self, songs):
self.songs = list(songs)
def __len__(self):
return len(self.songs)
pl = Playlist(["A", "B", "C"])
print(len(pl)) # 3
print(bool(Playlist([]))) # False
3.2 __getitem__:支持下标访问与迭代
实现 __getitem__ 后,对象支持 obj[index]。更妙的是,只要实现了 __getitem__,对象就自动支持 for 循环迭代 ------解释器会从索引 0 开始不断调用,直到抛出 IndexError 为止。
python
class Playlist:
def __init__(self, songs):
self.songs = list(songs)
def __len__(self):
return len(self.songs)
def __getitem__(self, index):
return self.songs[index]
pl = Playlist(["A", "B", "C"])
print(pl[0]) # A
print(pl[-1]) # C
print(pl[0:2]) # ['A', 'B'],切片也能工作
for song in pl:
print(song)
由于 self.songs 本身是列表,把索引直接透传给它,切片、负索引等能力就都免费获得了。
4. 相等性与哈希:__eq__ 与 __hash__
4.1 __eq__:定义相等语义
默认情况下,== 比较的是对象身份(即 is)。重写 __eq__ 可以让两个内容相同的对象判定为相等。
python
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
print(Point(1, 2) == Point(1, 2)) # True
这里返回 NotImplemented 而不是 False,是为了让 Python 有机会尝试对方的 __eq__,是更规范的做法。
4.2 __hash__:让对象可哈希
一旦重写了 __eq__,Python 会自动把 __hash__ 置为 None,对象就不可哈希 了,也就无法放进 set 或作为 dict 的键。
如果对象是不可变 的,应当同时实现 __hash__,且保证:两个相等对象的哈希值必须相同。
是否实现 __hash__,可以按下面的决策路径判断:
python
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
print(hash(Point(1, 2)) == hash(Point(1, 2))) # True
print(len({Point(1, 2), Point(1, 2)})) # 1
注意:如果对象是可变 的(属性会变化),就不要实现
__hash__,否则哈希值变化会破坏set/dict的内部结构。
5. 运算符重载
运算符重载让自定义对象支持 +、-、*、< 等运算,本质上是实现对应的魔法方法。
5.1 算术运算符
| 运算符 | 魔法方法 | 说明 |
|---|---|---|
+ |
__add__ |
加法 |
- |
__sub__ |
减法 |
* |
__mul__ |
乘法 |
/ |
__truediv__ |
真除法 |
% |
__mod__ |
取模 |
python
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x!r}, {self.y!r})"
print(Vector(1, 2) + Vector(3, 4)) # Vector(4, 6)
5.2 比较运算符
| 运算符 | 魔法方法 |
|---|---|
< |
__lt__ |
<= |
__le__ |
> |
__gt__ |
>= |
__ge__ |
借助 functools.total_ordering 装饰器,只需实现 __eq__ 和其中一个比较方法,其余会自动补齐。
python
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor):
self.major = major
self.minor = minor
def _key(self):
return (self.major, self.minor)
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._key() == other._key()
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._key() < other._key()
def __repr__(self):
return f"Version({self.major}, {self.minor})"
print(Version(1, 2) < Version(1, 10)) # True
print(Version(2, 0) >= Version(1, 9)) # True
5.3 反向运算符与就地运算符
- 反向运算符 :当左操作数不支持该运算时,Python 会尝试右操作数的
__radd__等方法。例如1 + vector会调用vector.__radd__(1)。 - 就地运算符 :
+=对应__iadd__,若未实现则回退到__add__并重新赋值。
python
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
if isinstance(other, (int, float)):
return Vector(self.x + other, self.y + other)
return NotImplemented
def __radd__(self, other):
return self.__add__(other)
def __repr__(self):
return f"Vector({self.x!r}, {self.y!r})"
print(Vector(1, 2) + 10) # Vector(11, 12)
print(10 + Vector(1, 2)) # Vector(11, 12)
6. 实战:实现一个自定义集合类
下面把前面学到的魔法方法综合起来,实现一个轻量的 SortedSet:内部用列表存储,保证元素唯一且有序,并支持 len()、下标访问、迭代、相等比较、哈希以及集合运算。
python
from bisect import bisect_left, insort
class SortedSet:
def __init__(self, iterable=()):
self._items = []
for item in iterable:
self.add(item)
# ---------- 容器协议 ----------
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __contains__(self, item):
i = bisect_left(self._items, item)
return i < len(self._items) and self._items[i] == item
def __iter__(self):
return iter(self._items)
# ---------- 表示 ----------
def __repr__(self):
return f"SortedSet({self._items!r})"
# ---------- 相等与哈希 ----------
def __eq__(self, other):
if not isinstance(other, SortedSet):
return NotImplemented
return self._items == other._items
def __hash__(self):
return hash(tuple(self._items))
# ---------- 运算符重载 ----------
def __or__(self, other):
"""并集:a | b"""
if not isinstance(other, SortedSet):
return NotImplemented
return SortedSet(self._items + other._items)
def __and__(self, other):
"""交集:a & b"""
if not isinstance(other, SortedSet):
return NotImplemented
return SortedSet(x for x in self._items if x in other)
def __sub__(self, other):
"""差集:a - b"""
if not isinstance(other, SortedSet):
return NotImplemented
return SortedSet(x for x in self._items if x not in other)
# ---------- 业务方法 ----------
def add(self, item):
i = bisect_left(self._items, item)
if i == len(self._items) or self._items[i] != item:
insort(self._items, item)
def discard(self, item):
i = bisect_left(self._items, item)
if i < len(self._items) and self._items[i] == item:
self._items.pop(i)
使用效果:
python
a = SortedSet([3, 1, 2, 2])
b = SortedSet([2, 4])
print(a) # SortedSet([1, 2, 3])
print(len(a)) # 3
print(a[0]) # 1
print(2 in a) # True
print(a | b) # SortedSet([1, 2, 3, 4])
print(a & b) # SortedSet([2])
print(a - b) # SortedSet([1, 3])
print(a == SortedSet([1, 2, 3])) # True
for x in a:
print(x) # 1 2 3
这个类虽然简单,但已经具备了一个「像内置类型一样好用」的容器应有的核心能力:可以用 len()、下标、in、for、==、|、&、- 等原生语法操作它。
SortedSet 的内部结构与对外能力可以这样理解:
7. 常见陷阱与最佳实践
__eq__与__hash__要成对考虑 :重写__eq__后__hash__会被置为None,不可变对象记得补上__hash__,可变对象则不要实现。- 返回
NotImplemented而非False:在比较和运算方法中,遇到不支持的类型应返回NotImplemented,让 Python 尝试反向操作。 __repr__优先于__str__:__repr__是调试和日志的主力,务必实现;__str__只在需要更友好的展示时补充。- 不要滥用魔法方法:只实现真正需要的协议,过度重载会让代码难以理解。
- 保持语义一致 :
__len__返回非负整数,__getitem__越界抛IndexError,__eq__满足自反、对称、传递------遵循内置类型的约定,使用者才不会踩坑。
8. 总结
魔法方法是 Python 数据模型的基石。掌握它们,你就能写出与内置类型无缝协作的类:
__init__/__str__/__repr__负责对象的创建与表示;__len__/__getitem__让对象支持容器语法;__eq__/__hash__定义相等与哈希语义;- 运算符重载让对象支持
+、<、|等运算; - 把这些组合起来,就能实现一个真正 Pythonic 的自定义集合类。
下次写类时,不妨先问自己一句:这个对象,能不能用更 Python 的方式被使用?