前言
Python 里凡是名字前后各带两条下划线的东西,例如__init__、__repr__、__dict__,社区俗称「魔术方法」(magic method)或 dunder(double underscore 的缩写)。它们不是给程序员随便调用的,而是语言本身在特定时刻会去调用的钩子:写len(obj)时,解释器实际调用的是obj.__len__();写obj + other时调用的是obj.__add__(other)。
围绕它们有两个典型误解。第一,把「内置方法」和「内置函数」混为一谈——len()是内置函数,__len__是你可以在自己类里实现的协议方法,两者是调用者与被调用者的关系。第二,以为__str__不定义就没法打印——实际上print(obj)会退而调用__repr__,而__repr__由object提供了一个默认实现,只是输出不好看而已。
本文讲清属性(attribute)与方法(method)两套机制,给出能直接运行的示例,并说明哪些行为是文档保证的、哪些只是当前实现的细节。示例基于 Python 3.8+。
一、先分清「属性」与「方法」
在 Python 的对象模型里,一切都在字典里。实例属性存在__dict__,方法本质上就是存在类字典里的函数对象,通过描述符协议在访问时绑定出self。
| 名字 | 类型 | 含义 | 在哪里 |
|---|
__dict__ | 属性 | 对象/类的属性字典 | 实例与类都有 |
__class__ | 属性 | 所属类 | 实例 |
__name__ | 属性 | 名字 | 类、函数、模块 |
__doc__ | 属性 | 文档字符串 | 类、函数、模块 |
__module__ | 属性 | 定义所在的模块名 | 类、函数 |
__qualname__ | 属性 | 限定名(含外层类名) | 类、函数(3.3+) |
__bases__ | 属性 | 直接父类元组 | 类 |
__mro__ | 属性 | 方法解析顺序 | 类 |
__slots__ | 属性 | 声明式属性表,省内存 | 类(可选) |
# 适用于 Python 3.8+
class Point:
"""二维点。"""
dims = 2
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.__dict__) # {'x': 1, 'y': 2}
print(p.__class__) # <class '__main__.Point'>
print(Point.__name__) # Point
print(Point.__doc__) # 二维点。
print(Point.__bases__) # (<class 'object'>,)
print(Point.dims, p.dims)# 2 2,类属性实例也能读到注意p.__dict__里没有dims——它是类属性。找属性时先查实例字典,再顺着__mro__查类字典。给实例赋值p.dims = 3会新建一个实例属性遮蔽类属性,不影响别的实例。
二、对象的构造与表示
| 方法 | 何时被调用 | 返回值约定 |
|---|
__new__(cls, ...) | 创建实例,最先执行 | 返回实例 |
__init__(self, ...) | 初始化实例 | 必须返回None |
__repr__(self) | repr()、交互式回显、调试 | str,尽量可还原 |
__str__(self) | str()、print() | str,面向人 |
__format__(self, spec) | f-string 与format() | str |
__hash__(self) | hash()、放进set/dict键 | int |
__eq__(self, other) | == | 任意(按约定给bool) |
# 适用于 Python 3.8+
class Money:
def __init__(self, amount, currency="CNY"):
self.amount = amount
self.currency = currency
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
def __str__(self):
return f"{self.amount} {self.currency}"
m = Money(12.5)
print(repr(m)) # Money(12.5, 'CNY')
print(str(m)) # 12.5 CNY
print(m) # 12.5 CNY,print 用的是 __str__
print([m]) # [Money(12.5, 'CNY')],容器用的是 __repr__经验法则:__repr__给开发者看(越像构造表达式越好),__str__给最终用户看。只定义__repr__时,str()会自动回退到它。
三、容器协议与迭代协议
想让自定义对象支持len()、下标、for循环、in判断,就实现对应协议。
| 方法 | 触发它的语法 |
|---|
__len__ | len(obj) |
__getitem__ | obj[key] |
__setitem__ | obj[key] = v |
__delitem__ | del obj[key] |
__contains__ | x in obj |
__iter__ | for x in obj、iter(obj) |
__next__ | next(obj) |
__call__ | obj(...) |
__enter__/__exit__ | with obj: |
# 适用于 Python 3.8+
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
def __len__(self):
return self.start
cd = Countdown(3)
print(len(cd)) # 3
print(list(cd)) # [3, 2, 1]
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
double = Multiplier(2)
print(double(21)) # 42
print(callable(double)) # True只要定义了__call__,实例就能像函数一样被调用,callable()也会返回True。
四、哪些行为是保证的
__dict__的存在、__mro__的线性化顺序、属性查找顺序都是文档保证的。但下面这些属于实现细节:
- 实例属性在
__dict__里的顺序,CPython 中通常是插入顺序,但不应依赖; __slots__能省内存,但它会去掉实例的__dict__,一旦定义,就只能给列出的名字赋值,无法再动态添加属性;- 定义了
__eq__却没定义__hash__,Python 会把该类的__hash__置为None,实例立即变成不可哈希、不能放进set或当字典键。
# 适用于 Python 3.8+
class Item:
def __init__(self, sku):
self.sku = sku
def __eq__(self, other):
return isinstance(other, Item) and self.sku == other.sku
__hash__ = None # 显式写出这个事实
print(Item("A") == Item("A")) # True
# {Item("A")} # TypeError: unhashable type常见坑点
坑 1:__init__里写了return。
❌
class C:
def __init__(self):
return 1✅__init__必须返回None(或不写 return),否则实例化时抛TypeError: __init__() should return None。想控制返回值要改__new__。
坑 2:只写__str__就以为调试输出够用。
❌ 容器、交互式回显用的都是__repr__,print(list_of_obj)里全是默认的内存地址形式(形如 object at 0x 加一串十六进制)。
✅ 至少实现__repr__,再按需实现__str__。
坑 3:定义__eq__后忘记__hash__。
❌ 对象放进set或当dict键时报unhashable type。
✅ 要么一起实现__hash__,要么明确该类设计为不可哈希。
坑 4:__eq__里没做类型检查。
❌
def __eq__(self, other):
return self.sku == other.sku # other 可能是任何类型✅ 先if not isinstance(other, Item): return NotImplemented,让解释器有机会走对方的比较逻辑。
坑 5:以为self.__x是私有的。
❌ 以为双下划线能阻止外部访问。
✅ 双下划线只做名字改写(name mangling),在类C里写self.__x实际是self._C__x,仍然能被访问。约定上"受保护"用单下划线_x表示,但它同样只是约定。
坑 6:给类加__slots__后还想动态加属性。
❌ 定义__slots__ = ("x",)之后写obj.y = 1,抛AttributeError。
✅ 把"y"也列进__slots__,或在__slots__里保留"__dict__"(这样就不再省内存了)。
坑 7:__len__返回了非整数。
❌def __len__(self): return "3"——len(obj)抛TypeError。
✅ 返回int(且非负);bool(obj)也可以用__len__的结果,为 0 即假,所以别让__len__有副作用。
坑 8:在__repr__里访问会触发大量计算或 IO 的属性。
❌__repr__里读数据库、打印大对象全量内容,导致调试时更卡。
✅__repr__保持轻量、无副作用,只输出标识性的少量字段。
总结
| 类别 | 代表方法 / 属性 | 一句话提醒 |
|---|
| 属性 | __dict__、__class__、__doc__、__mro__ | 查找顺序:实例字典 → 类 MRO |
| 构造 | __new__、__init__ | __init__必须返回None |
| 表示 | __repr__、__str__、__format__ | 至少实现__repr__ |
| 比较与哈希 | __eq__、__hash__ | 定义__eq__会连带清空__hash__ |
| 容器与迭代 | __len__、__getitem__、__iter__、__contains__ | 协议齐全才写得顺手 |
| 调用与上下文 | __call__、__enter__、__exit__ | with语句靠这两个 |
理解 dunder 的关键是转换视角:它们不是"要背的方法名",而是"语言在某个语法被使用时去找的钩子"。想知道某个类实现了哪些钩子,直接print(类名.__mro__)再help(类名),或者读object的文档;不要凭印象猜哪个魔术方法存在、参数是不是self, other。
(本主题与 Python 2 的主要差别在于:Python 2 里存在__unicode__、__nonzero__、__cmp__这些方法,在 Python 3 中分别被__str__、__bool__、__eq__体系取代。Python 2.7 已于 2020 年 1 月 1 日停止维护,新代码不要再用旧方法名。)