本文目录
在交互式里敲 len([1, 2, 3]) 得到 3,背后是列表类型的 __len__ 被调用。用户写的类默认没有这些钩子,所以 len(my_obj) 会报 TypeError——除非你自己实现对应的双下划线方法。
这类方法统称魔术方法(dunder,double underscore)。它们不打算被业务代码直接调用;解释器和内置函数在特定语法下自动查找并触发它们。搞清几个高频 dunder,就能让自定义类型「像内置类型一样」参与 print、==、len、算术运算等操作,而不必写一堆 to_string()、equals() 之类的旁路 API。
Python 没有运算符重载的单独关键字;+、==、len() 全是「语法 → 查 dunder → 调方法」的统一模型。内置 list、int 能做的事,你的类型也能按需挂钩——但只实现你真正需要的几个,比实现整张表更务实。
与 C++ 的 operator 不同,Python 的 dunder 是可选协议:不实现就得不到对应语法,或回退到默认行为(如 __eq__ 缺省时用 is)。这种「显式挂钩」让类型作者控制表面 API,而不必实现几十个 operator 才能编译通过。
repr 与 str:两种字符串形态
对象转字符串有两条路径,服务不同读者:
__repr__:给开发者看,目标是尽量明确、可反推对象内容;官方建议 ideally 能eval(repr(obj))重建(不总能做到)。__str__:给终端用户看,print(x)和str(x)用它;若未定义,回退到__repr__。
class Vector:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Vector({self.x!r}, {self.y!r})"
def __str__(self) -> str:
return f"({self.x}, {self.y})"
v = Vector(1.0, 2.0)
print(repr(v)) # Vector(1.0, 2.0)
print(str(v)) # (1.0, 2.0)
print(v) # (1.0, 2.0) — print 用 __str__容器展示元素、异常信息、调试输出里,常走 __repr__。面向用户的格式化走 __str__。团队约定:至少把 __repr__ 写准确,日志和 REPL 会舒服很多。
f"{v}" 默认调 __str__;f"{v!r}" 调 __repr__。在日志里打印未知对象时,用 %r 或 !r 往往比 str() 更安全——即便 __str__ 写坏了,__repr__ 仍可能可用。
len:对接 len() 与 truthiness
实现 __len__ 后,len(obj) 可用。若类未定义 __bool__,__len__ 还影响布尔值——长度为 0 为假,非 0 为真(与空容器直觉一致)。
class Playlist:
def __init__(self, tracks: list[str]) -> None:
self._tracks = list(tracks)
def __len__(self) -> int:
return len(self._tracks)
pl = Playlist(["a", "b"])
print(len(pl)) # 2
print(bool(pl)) # True
print(bool(Playlist([]))) # False不要指望 __len__ 返回负数或抛异常来「表示无效」——调用方假定它返回非负整数。需要「有没有有效数据」另写属性或 __bool__。
eq:== 比较;别和 is 混
a == b 调用 a.__eq__(b)(或对称的 b.__eq__(a) 回退)。未定义 __eq__ 时回退到身份比较(等价于 is)。定义了 __eq__ 却不定义 __hash__ 时,实例默认 __hash__ = None,不可哈希——不能放进 set / 当 dict 键。
class Money:
__slots__ = ("amount", "currency")
def __init__(self, amount: int, currency: str = "CNY") -> None:
self.amount = amount
self.currency = currency
def __eq__(self, other: object) -> bool:
if not isinstance(other, Money):
return NotImplemented
return self.amount == other.amount and self.currency == other.currency
a = Money(100)
b = Money(100)
print(a == b) # True
print(a is b) # False
print(Money(100) == Money(100, "USD")) # False对非预期类型返回 NotImplemented,让 Python 尝试反向比较或通知「无法比较」。若还要当 dict 键,需同时实现一致的 __hash__(且不可变字段)。
is 比较身份,永远不走 __eq__。值相等与同一对象是两回事:两个内容相同的 Money 实例 == 为真,但 is 为假。容器成员检测 x in bag 则依赖元素的 __eq__(或 __contains__),与 is 无关。
运算符挂钩:以 add 为例
a + b 尝试 a.__add__(b),若返回 NotImplemented 再试 b.__radd__(a)。这种「先左后右」规则让自定义类型与内置数字、字符串共存:
class Tag:
def __init__(self, label: str) -> None:
self.label = label
def __add__(self, other: "Tag") -> "Tag":
return Tag(f"{self.label}+{other.label}")
def __repr__(self) -> str:
return f"Tag({self.label!r})"
print(Tag("py") + Tag("dev")) # Tag('py+dev')同类还有 __sub__、__mul__、__lt__(配合 sorted)等——不必死记整张表,需要时查 数据模型文档。
反向方法 __radd__ 在左操作数不支持时启用,使 3 + MyInt(1) 有机会成功。数值类型扩展库常成对实现 __add__ 与 __radd__。若两边都返回 NotImplemented,解释器抛出 TypeError,而不是静默失败。
容器协议:getitem、iter
让对象支持下标和 for 循环,不必继承 list:
class Row:
def __init__(self, cells: list[str]) -> None:
self._cells = cells
def __getitem__(self, index: int) -> str:
return self._cells[index]
def __iter__(self):
return iter(self._cells)
row = Row(["id", "name"])
print(row[1]) # name
print(list(row)) # ['id', 'name']实现 __getitem__ 还能让对象支持切片(若 index 是 slice)。__iter__ 应返回迭代器;简单场景 return iter(self._cells) 即可。
配合 __contains__ 可自定义 in 运算符;不定义时,in 会尝试迭代并比较元素。容器类通常实现 __iter__ 与 __len__ 之一或两者,就能覆盖大部分使用场景。
数值转换:int 与 float
需要把对象传给 int()、float() 或格式化时,可实现对应转换方法:
class Percent:
def __init__(self, ratio: float) -> None:
self.ratio = ratio
def __float__(self) -> float:
return self.ratio
def __repr__(self) -> str:
return f"Percent({self.ratio})"
p = Percent(0.25)
print(float(p)) # 0.25
print(f"{float(p):.0%}") # 25%这类方法应返回内置数值类型,而不是再包一层自定义类,否则嵌套转换容易失控。
上下文与调用
with obj:→__enter__/__exit__(上下文管理器,后面专章展开)obj()→__call__(上一篇已讲可调用对象)
__enter__ 返回值常绑定到 as 变量;__exit__ 接收异常信息,返回真可压制异常。
上下文管理器与魔术方法的关系:资源管理(文件、锁)靠 with,背后仍是 dunder 协议。协程的 async with 则用 __aenter__ / __aexit__,属于异步专章内容,此处知道「还有异步版本」即可。
常见误解
| 误解 | 实际 |
|---|---|
| 应该实现所有 dunder | 只实现语法会用到的;缺省则回退或 TypeError |
__str__ 和 __repr__ 写一个就够 | 服务场景不同;调试靠 repr,展示靠 str |
__eq__ 会自动生成 hash | Python 3 中定义 __eq__ 往往使对象不可哈希 |
直接调 obj.__eq__(other) | 应使用 ==,让 Python 处理 NotImplemented 与反向比较 |
实现了 __len__ 就必须实现 __bool__ | 二者可选;有 __bool__ 时优先于 __len__ 决定 truthiness |
小实验:缺省行为
class Plain:
pass
p = Plain()
print(p == Plain()) # False — 不同实例,默认比身份
print(bool(p)) # True — 无 __len__/__bool__,非空对象即为真Plain 没有任何 dunder,仍参与 == 与 bool()——靠的是默认规则。加上 __eq__ 后相等语义变了,hash 规则也可能跟着变,改之前想清楚调用方是否依赖旧行为。
文档字符串里的「Supported operations」小节可以列出实现了哪些 dunder,比贴一张完整表格更友好。维护者一眼能看到类型是否支持 ==、是否可哈希、能否 len()。
第三方库 attrs、dataclasses 替你生成部分 dunder(如 __repr__、__eq__)。手写类时参考它们生成的样式,保持项目内对象表示一致,减少「有的类 repr 详细、有的简陋」的割裂感。
bool 与 hash:常与 eq 联动
自定义「空 / 满」语义时,可实现 __bool__ 覆盖默认的 __len__ 规则:
class Connection:
def __init__(self, open: bool) -> None:
self.open = open
def __bool__(self) -> bool:
return self.open
c = Connection(True)
print(bool(c)) # True若定义了 __eq__ 且希望实例可哈希(当 dict 键、set 元素),须保证:相等对象 hash 相等,且参与比较的字段在对象生命周期内不变:
class Point:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def __eq__(self, other: object) -> bool:
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
def __hash__(self) -> int:
return hash((self.x, self.y))
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # True
print({p1, p2}) # 一个元素
print({p1: "a"}[p2]) # a只写 __eq__ 不写 __hash__ 时,Python 3 会把 __hash__ 置为 None,实例自动不可哈希——这是常见默认,别与「忘了写 hash」混淆。
比较与排序:lt 等
要让 sorted(items) 按自定义规则排,实现 rich comparison 之一即可(通常 __lt__),并配合 @functools.total_ordering 生成其余比较:
from functools import total_ordering
@total_ordering
class Score:
def __init__(self, value: int) -> None:
self.value = value
def __eq__(self, other: object) -> bool:
if not isinstance(other, Score):
return NotImplemented
return self.value == other.value
def __lt__(self, other: "Score") -> bool:
return self.value < other.value
print(sorted([Score(3), Score(1), Score(2)], key=lambda s: s))== 与 < 挂钩不同语法:== 走 __eq__,< 走 __lt__,sort 还会用 < 比较元素。
容器与成员:contains
x in obj 优先找 __contains__;没有则回退到迭代并比较。自定义集合时可显式实现,避免低效扫描:
class TagSet:
def __init__(self, tags: set[str]) -> None:
self._tags = tags
def __contains__(self, item: object) -> bool:
return isinstance(item, str) and item in self._tags
def __repr__(self) -> str:
return f"TagSet({self._tags!r})"
ts = TagSet({"py", "dev"})
print("py" in ts) # True
print("java" in ts) # False调试建议:从 repr 开始
新类型优先实现 __repr__,再看 API 是否参与 == 或 len。REPL 里 repr(obj) 能立刻反映状态,比自定义 print 格式更通用。Code review 里自定义类缺 __repr__ 时,补一行往往是最划算的维护投资。
若类型会进日志、监控或错误报告,稳定的 __repr__ 比花哨的 __str__ 更重要——运维同事在 grep 日志时依赖可预测的格式。__str__ 可以随产品文案调整,__repr__ 宜保持稳定、可机器解析。
与内置类型的协作
自定义容器若只包装 list 或 dict,可考虑 __getitem__ / __iter__ 委托给内部对象,而不是重新实现整套协议。collections.abc 里的抽象基类可用来文档化「本类支持哪些协议」,运行时注册也依赖相应的 dunder。
序列化与 pickle 的提示
某些 dunder(如 __getstate__ / __setstate__)影响 pickle,属于进阶话题。日常 API 设计先保证 __repr__ 与 __eq__ 语义清晰;若对象要跨进程传递,再单独查 pickle 协议与 __reduce__。多数业务类型不必一开始就实现这一组。
调用协议回顾
__call__ 让实例像函数一样被调用;它与 __getitem__ 不同——后者对应 obj[key] 下标语法。Callable 与 Sequence/Mapping 在 collections.abc 里分开抽象;一个类可以同时实现 __call__ 与 __getitem__,分别服务两种语法。
实现 dunder 时尽量返回 NotImplemented 而不是抛 TypeError,除非确定没有回退路径——这与 __eq__、算术运算的协作规则一致。抛错会截断 Python 尝试 __radd__ 或反向比较的机会。
怎么选:少而准
| 你想让对象支持… | 优先实现 |
|---|---|
| 调试打印 | __repr__ |
| 用户可读输出 | __str__ |
len() / 空判断 | __len__ 或 __bool__ |
| 值相等 | __eq__(注意 hash 语义) |
| 算术 / 比较运算符 | 对应 __add__ 等 |
| 下标 / 迭代 | __getitem__ / __iter__ |
不必实现整张 dunder 表。先写 __repr__ 和与 API 直接相关的两三个,再按需扩展。
若你来自 Java,可把 dunder 类比为「运算符与内置函数调用的接口方法」——但 Python 不强制实现接口,缺了就不支持对应语法。若你来自 Ruby,则类似 to_s、==,只是 Python 用固定方法名与解释器硬连线。
扩展已有类型时,优先用组合加转发(wrapper 把 __getitem__ 委托给内部 list),而不是继承内置类型再 override——内置类型的 dunder 交互复杂,继承 list/dict 常踩坑。需要「像一个 list」时,实现协议并持有私有 list 往往更干净。
写单元测试时,可对 dunder 做表驱动:cases = [(lambda o: o + o, expected), ...],避免每个运算符复制粘贴断言。重点覆盖 __eq__ 与 __repr__,它们最容易被业务逻辑改坏而不自知。
回顾本篇四个核心:__repr__ 服务调试,__str__ 服务展示,__len__ 挂钩长度与空判断,__eq__ 挂钩值相等——其余 dunder 按 API 需要再加,不必一次凑齐整张清单。把协议当成可选能力表,而不是待办 checklist。下一篇描述符会接管「点号访问」这条线,与本篇的运算符、内置函数挂钩形成互补。
魔术方法是 Python 语法到底层协议的桥梁:print 找 __str__,调试找 __repr__,len() 找 __len__,== 找 __eq__。其余 dunder 在用到时再查官方数据模型文档即可——重点深讲胜过 endless list。