08-魔术方法
Python 版本要求:Python 3.11+ 贯穿项目:oop_demo/ 代码位置:
oop_demo/app/domain/book/model.py、oop_demo/app/domain/member.py、oop_demo/app/services/library.py测试验证:cd oop_demo && uv run pytest -k magic -v
概念铺垫
为什么需要魔术方法?
问题场景
你创建了 BookItem 类,希望它能:
print(book)显示友好格式,而非<app.domain.book.model.BookItem object at 0x...>book1 == book2按 ISBN 比较,而非按内存地址len(library)返回藏书数量"9780000000001" in library检查是否在馆sorted(books)按书名排序
不用魔术方法:
class BookItem:
def display(self) -> str:
status = "可借" if self._available else "已借出"
return f"《{self.title}》{self.author}({status})"
def equals(self, other) -> bool:
return self._isbn == other._isbn
b = BookItem("Python", "Guido", "9780000000001", 2020)
print(b.display()) # 必须记住方法名
print(b.equals(b2)) # 不能用 ==用魔术方法:
class BookItem:
def __str__(self) -> str:
status = "可借" if self._available else "已借出"
return f"《{self.title}》{self.author}({status})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, BookItem):
return NotImplemented
return self._isbn == other._isbn
b = BookItem("Python", "Guido", "9780000000001", 2020)
print(b) # 《Python》Guido(可借) ← 自然
print(b == b2) # True ← 直觉魔术方法的价值:让自定义类无缝融入 Python 的语法生态,len()、in、==、sorted() 等内置操作自动工作。
生活类比
魔术方法就像"魔法接口" — 给类安装了隐藏按钮。你定义了 __len__,Python 的 len() 就会自动按下这个按钮;你定义了 __eq__,== 运算符就会触发。这些按钮对使用者透明,他们不需要知道方法名,只需使用 Python 的内置操作。
L1 理解层:会用
核心原理
str vs repr
# oop_demo/app/domain/book/model.py
class BookItem:
def __str__(self) -> str:
status = "可借" if self._available else "已借出"
return f"《{self.title}》{self.author}({status})"
def __repr__(self) -> str:
return f"{self.__class__.__name__}(title={self.title!r}, isbn={self._isbn!r})"| 维度 | __str__ | __repr__ |
|---|---|---|
| 目标读者 | 终端用户 | 开发者/调试 |
| 触发方式 | print(obj)、str(obj) | repr(obj)、obj 在 REPL |
| 设计目标 | 易读 | 准确,理想情况下 eval(repr(obj)) 可重建 |
| 回退 | 未定义时用 __repr__ | 始终有默认 <类名 object at 0x...> |
最佳实践:至少定义 __repr__。如果只选一个,__repr__ 更重要。
eq + hash
# oop_demo/app/domain/book/model.py
class BookItem:
def __eq__(self, other: object) -> bool:
if not isinstance(other, BookItem):
return NotImplemented
return self._isbn == other._isbn
def __hash__(self) -> int:
return hash(self._isbn)关键规则:
| 规则 | 说明 |
|---|---|
| 相等对象必须有相同哈希 | a == b → hash(a) == hash(b) |
定义了 __eq__ 不定义 __hash__ → 自动不可哈希 | Python 3 的安全设计 |
__hash__ 的值必须不变 | 可变对象不应定义 __hash__ |
返回 NotImplemented 而非 False | 让 Python 尝试反向比较 |
demo_ch08 验证:
b1 = BookItem("Python", "Guido", "9780000000001", 2020)
b2 = BookItem("Python 入门", "不同作者", "9780000000001", 2021)
b1 == b2 # True(同 ISBN)
hash(b1) == hash(b2) # True
{b1, b2} # 长度 1(集合去重)lt 排序
# oop_demo/app/domain/book/model.py
class BookItem:
def __lt__(self, other: BookItem) -> bool:
return self.title < other.title只定义 __lt__,sorted() 和 list.sort() 就能工作。如果需要完整的比较运算符(<=、>、>=),配合 @functools.total_ordering 装饰器:
from functools import total_ordering
@total_ordering
class BookItem:
def __eq__(self, other): ...
def __lt__(self, other): ...
# 自动获得 __le__、__gt__、__ge__容器协议(len / iter / contains)
# oop_demo/app/services/library.py
class Library:
def __len__(self) -> int:
return len(self._books)
def __iter__(self):
return iter(sorted(self._books.values()))
def __contains__(self, isbn: str) -> bool:
return isbn in self._books| 魔术方法 | 触发操作 | demo_ch08 效果 |
|---|---|---|
__len__ | len(lib) | 返回藏书数量 |
__iter__ | for book in lib | 按书名排序迭代 |
__contains__ | "isbn" in lib | O(1) 查找 |
定义这三个方法后,Library 就能像 Python 内置容器一样使用。
贯穿实战
运行 demo_ch08 查看完整演示:
cd oop_demo && uv run python -m app
# 选择 8. 魔术方法或用测试验证:
cd oop_demo && uv run pytest -k magic -v测试覆盖:
test_str_shows_availability—__str__显示可借状态test_repr_contains_class_name—__repr__包含类名test_eq_by_isbn/test_ne_different_isbn—__eq__按 ISBN 比较test_hash_same_isbn—__hash__一致性test_lt_sorts_by_title—__lt__排序test_library_len/test_library_iter_sorted/test_library_contains— 容器协议test_member_str_and_repr— Member 的__str__/__repr__test_member_eq_by_member_id/test_member_hash— Member 的__eq__/__hash__
常用魔术方法速查
| 类别 | 方法 | 触发操作 |
|---|---|---|
| 对象表示 | __str__ | print(obj)、str(obj) |
__repr__ | repr(obj)、REPL 显示 | |
| 比较运算 | __eq__ | == |
__ne__ | !=(默认由 __eq__ 推导) | |
__lt__ | <、sorted() | |
__hash__ | hash(obj)、dict key、set | |
| 容器协议 | __len__ | len(obj) |
__iter__ | for x in obj、iter(obj) | |
__contains__ | x in obj | |
__getitem__ | obj[key] | |
__setitem__ | obj[key] = value | |
| 类型转换 | __bool__ | bool(obj)、if obj |
| 可调用 | __call__ | obj() |
L2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
至少定义 __repr__ | 调试必备,回退 __str__ | f"{cls}(field={val!r})" |
__eq__ 返回 NotImplemented | 支持反向比较,类型安全 | if not isinstance(other, X): return NotImplemented |
__eq__ + __hash__ 配对 | 让对象可放入 set/dict | 基于不可变字段计算 |
用 @total_ordering 简化排序 | 只需 __eq__ + __lt__ | sorted(books) |
容器类实现 __len__ + __iter__ + __contains__ | 融入 Python 生态 | len(lib)、isbn in lib |
| 魔术方法用语法触发,不直接调用 | 代码可读性 | 用 a == b 而非 a.__eq__(b) |
可变对象不定义 __hash__ | 哈希值改变后 dict/set 失效 | 或用 __hash__ = None 显式禁止 |
反模式:不要这样做
# ❌ 只定义 __eq__ 不定义 __hash__ —— 对象变不可哈希
class Book:
def __eq__(self, other):
return self.isbn == other.isbn
b1 = Book("9780000000001")
b2 = Book("9780000000001")
print(b1 == b2) # True
{b1, b2} # TypeError: unhashable type
# ✅ 同时定义 __eq__ 和 __hash__
class Book:
def __eq__(self, other):
if not isinstance(other, Book):
return NotImplemented
return self.isbn == other.isbn
def __hash__(self):
return hash(self.isbn)# ❌ __repr__ 返回不明确的字符串
def __repr__(self):
return "Book object" # 无用信息
# ✅ __repr__ 包含类名和关键字段
def __repr__(self):
return f"BookItem(title={self.title!r}, isbn={self._isbn!r})"# ❌ 可变对象定义 __hash__ — 哈希值改变后 set 找不到
class BadContainer:
def __init__(self):
self.items = []
def __eq__(self, other):
return self.items == other.items
def __hash__(self):
return hash(tuple(self.items)) # items 可变,哈希会变!
# ✅ 可变对象不定义 __hash__,或用 __hash__ = None
class GoodContainer:
def __init__(self):
self.items = []
def __eq__(self, other):
return self.items == other.items
__hash__ = None # 显式声明不可哈希# ❌ 魔术方法绕过语法直接调用
print(book.__str__()) # 错误
print(book.__eq__(book2)) # 错误
# ✅ 使用语法触发
print(str(book)) # 或 print(book)
print(book == book2)适用场景
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 需要打印友好输出 | __str__ + __repr__ | 至少定义 __repr__ |
| 需要按值比较(而非身份) | __eq__ | 返回 NotImplemented 支持子类 |
| 需要放入 set / dict key | __eq__ + __hash__ | 基于不可变字段 |
| 需要排序 | __lt__ + @total_ordering | 实现最小接口 |
| 自定义容器 | __len__ + __iter__ + __contains__ | 像内置容器一样使用 |
| 需要 with 语句支持 | __enter__ + __exit__ | 上下文管理器 |
| 需要可调用对象 | __call__ | 闭包的 OOP 替代 |
L3 专家层:深入
Python 如何实现:运算符重载与分发机制
二元运算符分发:__add__ vs __radd__
当 Python 执行 a + b 时:
a + b
│
▼
┌─────────────────────────────────────────────────────┐
│ 1. 尝试 type(a).__add__(a, b) │
│ ├── 返回 NotImplemented? │
│ │ └── 是 → 继续第 2 步 │
│ └── 返回实际值 → 完成 │
│ │
│ 2. 尝试 type(b).__radd__(b, a) ← 反向(反射)操作 │
│ ├── 返回 NotImplemented? │
│ │ └── 是 → 继续第 3 步 │
│ └── 返回实际值 → 完成 │
│ │
│ 3. 抛出 TypeError: unsupported operand type(s) │
└─────────────────────────────────────────────────────┘# 验证:__add__ 和 __radd__ 的调用顺序
class Left:
def __add__(self, other):
print("Left.__add__ called")
return NotImplemented # 返回 NotImplemented 触发反向
class Right:
def __radd__(self, other):
print("Right.__radd__ called")
return "result from Right"
l = Left()
r = Right()
print(l + r)
# Left.__add__ called
# Right.__radd__ called
# result from Right# 验证:实际项目中 NotImplemented 的正确用法
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented # 不是 NotImplementedError!
def __radd__(self, other):
# 当 other 不知道如何处理 Vector 时被调用
if isinstance(other, (int, float)):
return Vector(self.x + other, self.y + other)
return NotImplemented__getattr__ vs __getattribute__
| 方法 | 调用时机 | 优先级 | 用途 |
|---|---|---|---|
__getattribute__ | 每次属性访问都调用 | 最高(拦截一切) | 极少使用,极难正确实现 |
__getattr__ | 仅在常规查找失败时调用 | 最低(兜底) | 动态属性、代理、懒加载 |
obj.attr 查找流程:
│
▼
┌──────────────────┐
│ __getattribute__ │ ← 始终调用(如果定义了)
│ (自定义或默认) │ 自定义时必须调用 super().__getattribute__
└──────┬───────────┘
│
▼
┌──────────┐
│ 描述符 │ ← type(obj).__mro__ 查找
│ __dict__ │
│ MRO 链 │
└──────┬───┘
│ 没找到
▼
┌──────────┐
│__getattr__│ ← 仅当所有常规查找都失败时
└──────────┘# 验证:__getattr__ vs __getattribute__
class Demo:
def __init__(self):
self.x = 1
def __getattr__(self, name):
print(f"__getattr__ called for {name}")
return f"dynamic {name}"
def __getattribute__(self, name):
print(f"__getattribute__ called for {name}")
return super().__getattribute__(name)
d = Demo()
print(d.x) # __getattribute__ called → 返回 1
print(d.y) # __getattribute__ called (失败) → __getattr__ called → dynamic y# 警告:__getattribute__ 极易导致无限递归
class Broken:
def __getattribute__(self, name):
return self.__dict__[name] # 无限递归!
# self.__dict__ 自身触发 __getattribute__
class Fixed:
def __getattribute__(self, name):
return super().__getattribute__(name) # 正确性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
__eq__ | O(字段数) | 逐个字段比较 |
__hash__ | O(1) | 基于预计算哈希 |
__str__ / __repr__ | O(字段数) | 拼接字符串 |
__len__ | O(1) 或 O(n) | 取决于内部数据结构 |
__contains__ | O(1) 最佳 | 取决于内部数据结构 |
__lt__ + sorted() | O(n log n) | 排序算法自身 |
__getattribute__ 自定义 | 每次属性访问额外函数调用 | 有性能影响,谨慎使用 |
知识关联
魔术方法知识关联:
┌───────────────┐
│ 第 1 章:类 │
│ 基本定义 │
└───────┬───────┘
│
↓
┌───────────────┐
│ 第 8 章:魔术方法│
└───────┬───────┘
│
┌──────────────────┼──────────────────┐
↓ ↓ ↓
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ 字符串 │ │ 比较/哈希 │ │ 容器协议 │
│ __str__ │ │ __eq__ │ │ __len__ │
│ __repr__│ │ __hash__ │ │ __iter__ │
│ │ │ __lt__ │ │ __contains__│
└────┬────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
↓ ↓ ↓
调试输出 set/dict key for/in/len
日志记录 排序 迭代遍历
│ │ │
↓ ↓ ↓
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│__getattr│ │ __add__ │ │ __call__ │
│__getattr│ │ __radd__ │ │ 可调用对象 │
│ibute__ │ │ 运算符重载 │ │ │
└─────────┘ └─────────────┘ └─────────────┘自检清单
__str__和__repr__有什么区别?__str__面向用户(易读),__repr__面向开发者(准确);未定义__str__时用__repr__回退
为什么
__eq__要返回NotImplemented而不是False?NotImplemented让 Python 尝试反向比较(other.__eq__(self)),支持不同类型之间的比较
定义了
__eq__后为什么对象变成不可哈希的?- Python 3 的安全设计:相等的对象必须有相同哈希,自定义
__eq__后默认哈希不再保证这一点
- Python 3 的安全设计:相等的对象必须有相同哈希,自定义
容器协议最少需要几个魔术方法?
- 三个:
__len__(长度)、__iter__(迭代)、__contains__(成员检查,可选,未定义时退化为遍历)
- 三个:
魔术方法定义在实例上会被调用吗?
- 不会。Python 查找魔术方法时跳过实例
__dict__,直接从类字典开始查找
- 不会。Python 查找魔术方法时跳过实例
本章能力清单
学完本章后,你可以:
- [ ] 用
__str__/__repr__让对象打印友好 - [ ] 用
__eq__+__hash__实现基于字段的相等比较和哈希 - [ ] 用
__lt__+@total_ordering支持对象排序 - [ ] 用
__len__/__iter__/__contains__让自定义类像内置容器一样工作 - [ ] 理解魔术方法的调用机制和查找规则