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 知识体系总览

这一章的内容其实围绕一个核心思想:让自定义类拥有内置类型的行为。我画了一张图帮你理清关系:

魔法方法(下)知识体系 魔法方法 __call__ __getitem__ __setitem__ __enter__/__exit__ 应用场景 • 可调用对象 • 装饰器类 / 偏函数 应用场景 • 自定义容器类 • 支持下标/切片访问 应用场景 • 可变容器类 • 赋值时触发逻辑 应用场景 • 资源管理(文件/锁/连接) • 自动清理 / 异常处理 核心思想:让自定义类拥有内置类型的行为 可调用 · 可下标 · 可管理资源

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。


公众号:蓝海资料掘金营,微信deep3321