在 Python 中,魔法方法是对象行为的底层开关。通过实现特定名字的方法(如 getitem、call、enter),你定义的类就能像内置类型一样工作。本节聚焦三个常见方向:让你的对象像容器、像函数、像上下文管理器。
容器模拟:让对象像列表、字典一样使用
如果你希望自定义的类支持 obj[key]、for item in obj、len(obj) 这些操作,需要实现以下魔法方法:
len(self):返回容器中元素的数量,由len()调用。getitem(self, key):支持obj[key]读取,也支持切片和迭代(如果没有iter,Python 会回退到用索引从 0 开始的getitem来迭代)。setitem(self, key, value):支持obj[key] = value赋值。delitem(self, key):支持del obj[key]删除。contains(self, item):由in关键字触发,返回布尔值。若未实现,Python 会退而改用iter或getitem顺序查找,但性能可能较差。iter(self):返回一个迭代器对象,支持for item in obj和iter(obj)。
实际例子:读文件时,“字典式”访问配置
你可以写一个配置类,内部用一个字典存储数据,并通过 getitem 和 setitem 暴露出去,同时添加类型检查或默认值逻辑。典型如 config['host'] 这样的用法。
实用价值:设计领域模型时,将这些方法添加到业务类上,能让对象的使用方式一目了然,减少记忆成本。例如,封装数据库查询结果集时,实现 len 和 getitem 让它支持切片、迭代,调用方就完全不需要知道底层是列表还是生成器。
可调用对象:让实例像函数一样被调用
实现 *call(self, args, kwargs) 方法后,类的实例就可以直接像函数一样进行调用:instance(...)。
典型使用场景:
- 无状态的工具函数:需要维护某些配置或中间结果的稍复杂函数。
- 实现装饰器:基于类实现装饰器时,
init接收装饰器参数,call接收并调用被装饰的函数。 - 状态机/处理器链:对象内部持有状态(如计数器、缓存),每次调用时根据当前状态做出不同反应。
简单示例:
class Repeater:
def __init__(self, times):
self.times = times
def __call__(self, word):
return word * self.times
repeat3 = Repeater(3)
print(repeat3("Go")) # 输出 GoGoGo
这个例子展示了 call 如何将对象使用体验变成直观的函数调用,同时又保留了配置 times 的能力。
上下文管理:安全地管理资源
上下文管理器让 with 语句变得强大,通常用来管理文件、锁、数据库连接等需要“进入时获取,离开时自动释放”的资源。实现方式有两种:
1. 魔法方法 enter 和 exit
enter(self):进入with块时调用,返回值会赋给as后的变量。exit(self, exc_type, exc_val, exc_tb):离开with块时必然执行,即使发生异常。三个参数为异常信息,可决定是否压制异常(返回True压制)。
典型示例:自定义文件操作计时器
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.time() - self.start
print(f"耗时: {elapsed:.2f}秒")
# 不压制任何异常,返回 False(或 None)
return False
with Timer() as t:
# 要做的事情
time.sleep(1.5)
# 输出:耗时: 1.50秒
2. contextlib 库的简化方式
对于简单的上下文管理器,可以用 contextlib.contextmanager 装饰一个生成器函数,yield 之前的部分相当于 enter,之后的部分相当于 exit。代码更少,但只能处理同步场景。
实用价值:上下文管理器是 Python 中最佳的“资源保险”手段。无论 with 块内是否抛出异常,退出时总会执行清理代码,有效避免文件未关闭、锁未释放等问题。在生产代码中,数据库会话、网络连接、临时文件的操作都应使用上下文管理器来保证可靠性。
这三种魔法方法共同展示了 Python 的协议导向设计:只要你的类遵守规定的接口,就能无缝融入 Python 的语言特性,让代码更流畅、更安全、更具表现力。