从基类包装派生类方法



假设我有一个基类和一个派生类,如下所示:

class A:
def foo(self):
pass
class B(A):
def foo(self):
pass

我希望封装foo调用B实例的调用。不允许修改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个闭包的成本。

有更好的方法吗?也许是基于元类的方法?

除非你打算同时拥有数千个这样的实例,否则你不应该担心这样做的资源使用-与运行的Python应用程序将使用的其他资源相比,它是相当小的。

只是为了比较:asyncio协程和任务是一种可以在一个进程中创建数千个对象的对象,并且它只是"ok",将具有类似的开销。

但是,由于您已经控制了B类,因此有几种方法可以做到这一点,而无需诉诸"猴子补丁";-这将在B被创建后对其进行修改。当一个人必须修改一个他们无法控制代码的类时,这通常是唯一的选择。

当从B实例中检索方法时,以一种懒惰的方式自动包装方法,甚至可以避免这种情况-并且肯定比在基类__init__中包装更优雅:

如果你事先知道你必须包装的方法,并且确定它们是在你控制的类的子类中实现的,这可以通过制作一个专门的__getattribute__方法来实现:这样,方法只有在即将使用时才被包装。

from functools import wraps, partial
def _capture(f):  # <- there is no need for this to be inside __getattribute__
# unless the wrapper is to call `super()`
@wraps(f)
def wrapper(self, *args, **kwargs):
print("wrapped")
return f(*args, **kwargs) 
# ^ "f" is already bound when we retrieve it via super().__getattribute__
# so, take care not to pass "self" twice. (the snippet in the question
# body seems to do that)
return wrapper
class A:

def __getattribute__(self, name):
fnames = {"foo", }
attr = super().__getattribute__(name)
if name in fnames:
# ^ maybe add aditional checks, like if attr is a method,
# and if its origin is indeed in a
# class we want to change the behavior
attr = partial(_capture(attr), self)  
# ^ partial with self as the first parameter
# has the same effect as calling __get__ passing
# the instance to bind the method

return attr

class B(A):
def foo(self):
pass

至于在创建B时包装foo,这可以使用更少的资源-虽然它可以在元类中完成,但从Python 3.6开始,__init_subclass__特殊方法可以处理它,而不需要自定义元类。

然而,如果代码可能在class C(B):中进一步子类B,这将再次覆盖foo,则这种方法可能会很棘手:如果方法使用super()调用基类中的foo,则可以多次调用包装器。避免包装器中的代码运行多次需要一些复杂的状态处理(但它可以毫无意外地完成)。


from functools import wraps
def _capture(f):
@wraps(f)
def wrapper(self, *args, **kwargs):
print("wrapped")
return f(self, *args, **kwargs) 
# ^ "f" is retrieved from the class in __init_subclass__, before being 
# bound, so "self" is forwarded explicitly
return wrapper
class A:
def __init_subclass__(cls, *args, **kw):
super().__init_subclass__(*args, **kw)
fnames = {"foo",}
for name in fnames:
if name not in cls.__dict__:
continue
setattr(cls, name, _capture(getattr(cls, name)))
# ^no need to juggle with binding the captured method:
# it will work just as any other method in the class, and
# `self` will be filled in by the Python runtime itself.

# / also, no need to return anything.


class B(A):
def foo(self):
pass

follow @jsbueno answer

使用

types.MethodType(attr, self)

partial(_capture(attr), self)

,因为partial没有描述符,所以返回的属性没有绑定到类实例。只需要将返回包装器修改为

def wrapper(self, *args, **kwargs):
print("wrapped")
return f(*args, **kwargs) 

最新更新