第 7 章 · 魔法方法与运算符重载
本章目标:理解 Python 双下划线方法(dunder) 的调用时机;掌握 __str__ / __repr__、比较与哈希、容器协议、上下文管理器与 __call__;能为 shop-demo 的 Product、Cart 写出「Python 风格」的类;知道何时该重载、何时不该过度设计。
学时建议:3~4 小时(含 1 小时跟练)
前置:ch06 面向对象(类、__init__、@property)。
7.1 什么是魔法方法
Python 在特定操作时自动调用以 __ 开头和结尾的方法,例如:
| 你写的代码 | Python 实际调用 |
|---|---|
print(obj) | obj.__str__() |
repr(obj) | obj.__repr__() |
len(cart) | cart.__len__() |
a == b | a.__eq__(b) |
for x in cart | cart.__iter__() |
with open(...) | __enter__ / __exit__ |
用户代码 魔法方法层 内置行为
print(cart) → cart.__str__() → 控制台可读字符串
cart["BK-1"] → cart.__getitem__ → 像字典一样取值
原则:魔法方法让自定义类融入 Python 语法;不是「炫技」,而是让 API 更自然。
7.2 __str__ 与 __repr__
| 方法 | 受众 | 目标 |
|---|---|---|
__str__ | 终端用户 / 日志 | 可读、简短 |
__repr__ | 开发者 / 调试 | 尽量能 eval 重建对象,至少含类名与关键字段 |
class Product:
def __init__(self, sku: str, title: str, price: float):
self.sku = sku
self.title = title
self.price = price
def __str__(self) -> str:
return f"{self.title}(¥{self.price:.2f})"
def __repr__(self) -> str:
return f"Product(sku={self.sku!r}, title={self.title!r}, price={self.price})"
p = Product("BK-001", "Python 基础", 59.9)
print(str(p)) # Python 基础(¥59.90)
print(repr(p)) # Product(sku='BK-001', title='Python 基础', price=59.9)
最佳实践:至少实现 __repr__;容器类在 __repr__ 里展示子项摘要。
7.3 比较运算:__eq__、__lt__ 与 __hash__
相等 __eq__
默认比较对象 id(同一实例才相等)。业务上常按 sku 判等:
def __eq__(self, other: object) -> bool:
if not isinstance(other, Product):
return NotImplemented
return self.sku == other.sku
排序 __lt__
配合 sorted() / @total_ordering:
from functools import total_ordering
@total_ordering
class Product:
# ... __init__ ...
def __lt__(self, other: "Product") -> bool:
if not isinstance(other, Product):
return NotImplemented
return self.price < other.price
哈希 __hash__
对象作为 dict 键 或 set 元素 时需可哈希;可变对象若定义了 __eq__,通常应设 __hash__ = None(不可哈希),或保证「相等对象哈希相同」且字段不可变。
# 不可变 sku 作键的简化写法(教学用)
def __hash__(self) -> int:
return hash(self.sku)
| 场景 | 建议 |
|---|---|
| 按 sku 去重 | __eq__ + __hash__ 基于 sku |
| 可变购物车行 | 不要放进 set;或只用 id 哈希 |
7.4 容器协议:__len__、__getitem__、__iter__
让 Cart 支持 len()、下标访问与 for 循环:
class Cart:
def __init__(self):
self._lines: list[tuple[Product, int]] = []
def add(self, product: Product, qty: int = 1) -> None:
self._lines.append((product, qty))
def __len__(self) -> int:
return len(self._lines)
def __getitem__(self, index: int):
return self._lines[index]
def __iter__(self):
return iter(self._lines)
def total(self) -> float:
return sum(p.price * q for p, q in self._lines)
cart = Cart()
cart.add(p, 2)
print(len(cart)) # 1
print(cart[0][0].title)
for product, qty in cart:
print(product, qty)
7.5 上下文管理器:__enter__ 与 __exit__
with 语句保证进入/退出成对执行,适合锁、文件、数据库连接:
class OrderWriter:
"""向文件追加订单行,退出时 flush。"""
def __init__(self, path: str):
self.path = path
self._file = None
def __enter__(self):