一、引言:揭开"魔法"的面纱

当你写下 len(my_list)my_obj + 1 时,Python 是如何知道怎么做的?

答案是一套约定俗成的"暗号"系统——特殊名称(Magic Methods)。它们是 Python 对象与解释器之间的通信协议,以双下划线开头和结尾(因此也叫 Dunder Methods,Double UNDERscore)。

掌握这些暗号,你就能让自定义对象像内置类型一样工作。

二、基础篇:对象的"自我介绍"与"生命周期"

2.1 对应关系

特殊名称 对应的内置功能 说明
__init__ 对象初始化 构造函数,创建实例时调用
__str__ str() / print() 面向用户的友好展示
__repr__ repr() / 交互式显示 面向开发者的精确描述

2.2 代码演示

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

def __str__(self):
return f"{self.name}, age {self.age}"

def __repr__(self):
return f"Person(name={self.name!r}, age={self.age})"

p = Person("Alice", 30)

print(p) # Alice, age 30 —— 调用 __str__
print(str(p)) # Alice, age 30
print(repr(p)) # Person(name='Alice', age=30) —— 调用 __repr__

在交互式命令行中直接输入 p,显示的是 __repr__ 的结果;print(p) 调用的是 __str__

三、进阶篇:让对象像数字一样运算

3.1 对应关系

特殊名称 对应的操作符 说明
__add__ + 加法
__sub__ - 减法
__mul__ * 乘法
__truediv__ / 除法
__floordiv__ // 整除
__mod__ % 取模
__pow__ ** 幂运算
__eq__ == 等于
__lt__ < 小于
__le__ <= 小于等于
__gt__ > 大于
__ge__ >= 大于等于
__ne__ != 不等于

3.2 代码演示

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

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

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

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

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2) # Vector(4, 6) —— 调用 __add__
print(v1 == v2) # False —— 调用 __eq__
print(v1 == Vector(1, 2)) # True

当你写 v1 + v2 时,Python 实际上调用的是 v1.__add__(v2)操作符只是语法糖,魔法方法才是真正的执行者。

四、高阶篇:让对象像容器一样工作

4.1 对应关系

特殊名称 对应的内置功能 说明
__len__ len() 获取长度
__getitem__ obj[key] 索引访问
__setitem__ obj[key] = value 索引赋值
__delitem__ del obj[key] 索引删除
__contains__ x in obj 成员检测
__iter__ for x in obj 迭代协议
__next__ next() 迭代器协议

4.2 代码演示

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 CustomList:
def __init__(self, *items):
self._items = list(items)

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

def __getitem__(self, index):
return self._items[index]

def __setitem__(self, index, value):
self._items[index] = value

def __contains__(self, item):
return item in self._items

def __iter__(self):
return iter(self._items)

def __repr__(self):
return f"CustomList({self._items})"

cl = CustomList(10, 20, 30)

print(len(cl)) # 3 —— 调用 __len__
print(cl[0]) # 10 —— 调用 __getitem__
cl[1] = 99 # 调用 __setitem__
print(10 in cl) # True —— 调用 __contains__
print(list(cl)) # [10, 99, 30] —— 调用 __iter__

for item in cl: # 调用 __iter__
print(item)

实现了 __len____getitem__,你的对象就能被 len() 度量和被索引访问。实现了 __iter__,就能被 for 循环遍历。这就是 Python 的**协议(Protocol)**思想——不需要继承特定基类,只要实现了对应的方法,就拥有了对应的能力。

五、属性访问的"守门员"

5.1 对应关系

特殊名称 触发时机 说明
__getattr__ 访问不存在的属性 兜底机制
__getattribute__ 访问任何属性 拦截所有属性访问
__setattr__ 设置任何属性 拦截赋值操作
__delattr__ 删除属性 拦截删除操作

5.2 代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ReadOnly:
def __init__(self, **kwargs):
object.__setattr__(self, '_data', kwargs)

def __getattr__(self, name):
if name in self._data:
return self._data[name]
raise AttributeError(f"'{type(self).__name__}' 没有属性 '{name}'")

def __setattr__(self, name, value):
raise AttributeError("此对象是只读的,不允许修改属性")

ro = ReadOnly(name="Alice", age=30)
print(ro.name) # Alice —— 调用 __getattr__
# ro.name = "Bob" # AttributeError: 此对象是只读的
# ro.city = "NYC" # AttributeError: 此对象是只读的

__getattr__ 只在属性不存在时被调用(兜底),而 __getattribute__ 在每次属性访问时都被调用(更强大但也更危险,容易导致无限递归)。

六、完整对照表

特殊名称 对应的内置功能 分类
__init__ 对象初始化 生命周期
__str__ str() / print() 字符串表示
__repr__ repr() 字符串表示
__add__ + 算术运算
__sub__ - 算术运算
__mul__ * 算术运算
__eq__ == 比较运算
__lt__ < 比较运算
__len__ len() 容器协议
__getitem__ obj[key] 容器协议
__setitem__ obj[key] = val 容器协议
__contains__ x in obj 容器协议
__iter__ for x in obj 迭代协议
__next__ next() 迭代协议
__getattr__ 访问不存在的属性 属性访问
__setattr__ 设置属性 属性访问
__call__ obj() 可调用对象
__enter__ / __exit__ with 语句 上下文管理
__hash__ hash() 哈希协议

七、最佳实践

  1. 不要过度使用魔法方法——保持代码可读性,只在需要自定义行为时实现
  2. 优先实现 __repr__——它是最基础的调试工具,也是容器打印的默认选择
  3. 实现容器类型时务必遵循迭代协议——__iter__ + __next____getitem__
  4. 算术运算注意反向方法——__radd__ 等方法处理 1 + obj 的情况
  5. __getattr____getattribute__ 不要混淆——前者是兜底,后者是全拦截

魔法方法的本质是协议——Python 不关心你的类继承自谁,只关心你是否实现了对应的方法。这种"鸭子类型"的设计哲学,正是 Python 灵活性的根源。