Python不修改派生类时 基类包装派生类方法的内存优化方案
假设存在如下基类与派生类定义:
class A: def foo(self): pass class B(A): def foo(self): pass
需求:对B类实例发起的foo方法调用进行包装,不允许修改B类的任何代码(B类不属于我方维护范畴)。
现有实现方案如下:
class A: def __init__(self): fnames = ["foo"] for fname in fnames: def capture(f): def wrapper(*args, **kwargs): print("wrapped") return f(*args, **kwargs) return wrapper meth = capture(getattr(self, fname)) bound = meth.__get__(self) setattr(self, fname, bound) class B(A): def foo(self): print("b") o = B() o.foo() print(o.foo)
该方案可以实现预期的包装效果,但存在明显的内存效率问题:当前实现中o.foo为<bound method A.__init__.<locals>.capture.<locals>.wrapper of <__main__.B object at 0x10523ffd0>>,每创建一个实例都会生成2层闭包,当实例量级较大时会产生大量冗余内存开销。
不需要使用元类,以下两种方案都可以避免逐实例生成闭包的内存浪费:
方案1:类级别统一包装(推荐,性能最优)
直接在A类层面替换需要包装的方法,包装逻辑仅在类定义时生成一次,所有实例共用,无额外实例内存开销:
from functools import wraps class A: def foo(self): pass # 类级别包装,仅执行1次 _origin_a_foo = A.foo @wraps(_origin_a_foo) def wrapped_foo(self, *args, **kwargs): print("wrapped") # 沿MRO链调用子类的原始foo实现 return super(type(self), self).foo(*args, **kwargs) A.foo = wrapped_foo class B(A): def foo(self): print("b") # 测试 o = B() o.foo() # 依次输出 wrapped、b print(o.foo) # 输出 <bound method A.wrapped_foo of <__main__.B object at 0x...>>
这个方案完全没有修改B类代码,符合需求限制。原理是Python方法调用遵循MRO(方法解析顺序)规则,替换A类的foo为包装版本后,B实例调用foo时会优先命中A类上的包装逻辑,包装内部通过super调用MRO链中A类之后的实现(也就是B类自定义的foo),行为和预期完全一致,是所有方案中运行性能、内存效率最高的实现。
方案2:getattribute 拦截(适合动态配置场景)
如果需要包装的方法列表需要动态调整,可以在A类重写__getattribute__做统一拦截,同样不需要在__init__中为每个实例生成闭包:
# 可动态修改的要包装的方法集合 WRAPPED_METHODS = {"foo"} class A: def foo(self): pass def __getattribute__(self, name): attr = super().__getattribute__(name) if name in WRAPPED_METHODS and callable(attr): def wrapper(*args, **kwargs): print("wrapped") return attr(*args, **kwargs) return wrapper return attr class B(A): def foo(self): print("b")
该方案的缺点是每次实例属性访问都会触发__getattribute__的判断逻辑,运行性能略低于方案1,仅适合需要灵活调整包装方法的场景。
关于元类实现的说明
不推荐使用元类实现该需求:元类的作用是控制类的创建过程,本场景下我们不需要修改B类的定义本身,使用元类只会徒增代码复杂度,最终实现的效果和方案1没有区别,没有额外收益。
内容的提问来源于stack exchange,提问作者xrisk

