为何Python的with关键字会忽略实例上的猴子补丁方法?
问题:实例级猴子补丁无法修改上下文管理器行为,类级补丁却生效的原因
想修改现有类实例(比如数据库连接对象)的上下文管理器行为,一开始尝试给实例的__enter__和__exit__做猴子补丁,但没生效;修改类(包括替换实例的__class__)却能达到预期效果。想知道with关键字这种行为的本质原因,看了PEP 343也没找到答案。
可运行示例代码
import types class My: def __enter__(self): print('enter') def __exit__(self, a, b, c): print('exit') def add_behavior_to_context_manager(c): # 不生效 c_enter = c.__enter__ c_exit = c.__exit__ def __enter__(self): print('enter!') c_enter() return c def __exit__(self, exc_type, exc_value, exc_tb): c_exit(exc_type, exc_value, exc_tb) print('exit!') c.__enter__ = types.MethodType(__enter__, c) c.__exit__ = types.MethodType(__exit__, c) return c def add_behavior_by_modifying_class(c): # 生效 class MonkeyPatchedConnection(type(c)): def __enter__(self): print('enter!') return super().__enter__() def __exit__(wrapped, exc_type, exc_value, exc_tb): super().__exit__(exc_type, exc_value, exc_tb) print('exit!') c.__class__ = MonkeyPatchedConnection return c my = add_behavior_to_context_manager(My()) print('直接调用实例方法时符合预期: ') my.__enter__() my.__exit__(None, None, None) print('\nwith语句忽略了实例上的方法: ') with add_behavior_to_context_manager(My()): pass print('\nwith语句会调用类上的方法: ') with add_behavior_by_modifying_class(My()): pass
输出结果
直接调用实例方法时符合预期: enter! enter exit exit! with语句忽略了实例上的方法: enter exit with语句会调用类上的方法: enter! enter exit exit!
原因解析
核心原因是:Python的with语句在查找__enter__和__exit__这类特殊方法时,会直接从实例的类(及继承链)中查找,完全跳过实例自身的属性字典。
特殊方法的查找机制
Python解释器对双下划线开头结尾的特殊方法,采用了和普通方法不同的查找逻辑:
- 普通方法调用(比如
my.__enter__())会先检查实例的属性字典,如果存在绑定的方法就优先调用; - 但
with语句触发的特殊方法调用,是解释器直接通过type(obj).__enter__(obj)的方式执行的——它直接去实例的类里找方法,根本不会看实例自身有没有绑定同名方法。
这就解释了为什么两种补丁方式结果不同:
- 实例级补丁:你给实例绑定的新
__enter__和__exit__只存在于实例的属性字典里,with语句根本不看这里,所以还是调用类里的原方法;但手动调用实例方法时,会优先用实例上的绑定方法,所以能看到预期输出。 - 类级补丁(替换
__class__):你创建了原类的子类并重写了特殊方法,然后把实例的__class__指向这个子类。此时type(obj)就是这个子类,with语句查找类的特殊方法时,自然会找到子类里重写的版本,因此生效。
关于修改__class__的合理性
这种方式是Python允许的操作,针对单个实例修改行为时是可行的,但要注意潜在风险:
- 不会影响原类的其他实例;
- 如果代码中有基于
isinstance(obj, My)这类类型检查的逻辑,替换__class__后会导致检查失败; - 也可以考虑用代理类包裹原实例,或者用装饰器包装上下文管理器,来达到类似效果。
内容的提问来源于stack exchange,提问作者user443854
相关产品推荐
相关产品推荐

