10. 魔法方法(下):__call__、__getitem__、__setitem__、__enter__与__exit__(上下文管理器)
好,咱们接着聊魔法方法。上一章我们把构造、析构、字符串表示这些基础魔法方法讲透了。这一章要聊的几个,我个人觉得是真正能让你写出「优雅代码」的关键。
说白了,魔法方法就是让自定义类表现得像内置类型一样自然。你想想看,为什么列表能用 obj[0] 取值?为什么函数能直接 func() 调用?为什么 with open() as f 能自动关闭文件?
嗯,答案就在这四个魔法方法里。
10.1 __call__:让对象像函数一样调用
先问个问题:对象能不能像函数一样被调用?
能。只要实现了 __call__ 方法。
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return self.n + x
add_five = Adder(5)
print(add_five(10)) # 输出 15
print(add_five(3)) # 输出 8
你看,add_five 明明是个对象,却可以直接 add_five(10) 调用。这就是 __call__ 的魔力。
__call__ 实现「可配置的函数」。比如一个数据清洗管道,每个清洗步骤都是一个可调用对象,参数在初始化时固定,调用时只传数据。代码结构清晰多了。
__call__ 最常见的应用场景有两个:
- 函数装饰器类:类实现
__call__后,可以像函数装饰器一样使用 - 偏函数模拟:固定部分参数,生成可调用的新对象
class Logger:
def __init__(self, prefix):
self.prefix = prefix
def __call__(self, func):
def wrapper(*args, **kwargs):
print(f"[{self.prefix}] 调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
@Logger("DEBUG")
def greet(name):
print(f"你好, {name}")
greet("小明")
# 输出:
# [DEBUG] 调用 greet
# 你好, 小明
10.2 __getitem__ 与 __setitem__:让对象支持下标操作
为什么列表能用 lst[0] 取值?为什么字典能用 d['key'] 赋值?
因为内置类型实现了 __getitem__ 和 __setitem__。你的自定义类也可以。
class MyList:
def __init__(self):
self._data = []
def __getitem__(self, index):
print(f"取值: index={index}")
return self._data[index]
def __setitem__(self, index, value):
print(f"赋值: index={index}, value={value}")
self._data[index] = value
def append(self, item):
self._data.append(item)
ml = MyList()
ml.append(10)
ml.append(20)
ml.append(30)
print(ml[1]) # 触发 __getitem__
ml[2] = 99 # 触发 __setitem__
print(ml[2]) # 输出 99
__getitem__ 忘了实现 __setitem__。结果代码跑起来,读数据没问题,一赋值就报 TypeError: 'MyList' object does not support item assignment。排查了半天才发现少了 __setitem__。嗯,这两个方法通常成对出现,别漏了。
这两个方法还能支持切片操作:
class MyRange:
def __init__(self, start, end):
self._data = list(range(start, end))
def __getitem__(self, index):
if isinstance(index, slice):
# 处理切片
return self._data[index]
return self._data[index]
mr = MyRange(0, 10)
print(mr[2:5]) # 输出 [2, 3, 4]
10.3 __enter__ 与 __exit__:上下文管理器的核心
这是我最喜欢的一组魔法方法。为什么?因为它解决了资源管理的痛点。
你想想看,打开文件要记得关闭,获取锁要记得释放,建立数据库连接要记得断开。这些「记得」一旦忘了,轻则资源泄漏,重则程序崩溃。
上下文管理器就是来救你的。
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print(f"打开文件: {self.filename}")
self.file = open(self.filename, self.mode)
return self.file # 这个返回值会赋给 as 后面的变量
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"关闭文件: {self.filename}")
if self.file:
self.file.close()
# 返回 False 表示不处理异常,让异常继续传播
# 返回 True 表示异常已被处理,程序继续执行
return False
# 使用
with FileManager("test.txt", "w") as f:
f.write("Hello, World!")
# 离开 with 块时自动调用 __exit__
__exit__ 的三个参数很重要:
exc_type:异常类型(如果没有异常则为 None)exc_val:异常实例exc_tb:异常追踪信息
__exit__ 返回 True 会吞掉异常,返回 False 则异常继续传播。我个人习惯除非有特殊需求,否则返回 False,让异常自然传播,方便调试。
来看一个更实用的例子——数据库连接管理器:
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
print(f"连接数据库: {self.db_name}")
self.connection = {"name": self.db_name, "connected": True}
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"断开数据库: {self.db_name}")
self.connection["connected"] = False
if exc_type:
print(f"发生异常: {exc_val}")
return False
with DatabaseConnection("mydb") as conn:
print(f"连接状态: {conn['connected']}")
# 模拟异常
# raise ValueError("数据错误")
print("程序结束")
10.4 知识体系总览
这一章的内容其实围绕一个核心思想:让自定义类拥有内置类型的行为。我画了一张图帮你理清关系:
10.5 综合示例:一个智能缓存容器
把今天学的魔法方法串起来,写一个实用的缓存容器:
class SmartCache:
def __init__(self, max_size=5):
self._cache = {}
self._max_size = max_size
self._access_count = 0
def __getitem__(self, key):
self._access_count += 1
print(f"[读取] key={key}, 总访问次数={self._access_count}")
return self._cache[key]
def __setitem__(self, key, value):
if len(self._cache) >= self._max_size:
# 简单淘汰策略:删除第一个
old_key = next(iter(self._cache))
print(f"[淘汰] 移除 {old_key}")
del self._cache[old_key]
self._cache[key] = value
print(f"[写入] key={key}, value={value}")
def __call__(self, key, default=None):
"""如果 key 存在返回 value,否则返回 default"""
return self._cache.get(key, default)
def __enter__(self):
print("[上下文] 进入缓存管理器")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"[上下文] 退出缓存管理器,当前缓存大小: {len(self._cache)}")
if exc_type:
print(f"[异常] {exc_val}")
return False
# 使用示例
with SmartCache(max_size=3) as cache:
cache["a"] = 1
cache["b"] = 2
cache["c"] = 3
cache["d"] = 4 # 触发淘汰
print(cache["b"]) # 读取
print(cache("e", "未找到")) # 安全读取
__setitem__ 里处理淘汰逻辑。结果缓存满了直接报 MemoryError。嗯,写容器类一定要考虑边界情况,尤其是容量限制。
好了,这一章的内容就到这里。四个魔法方法,各自解决一类问题:__call__ 让对象可调用,__getitem__ 和 __setitem__ 让对象可下标操作,__enter__ 和 __exit__ 让对象支持上下文管理。掌握了它们,你的代码会变得更 Pythonic。