这个答案基于@AlexHall 和@juanpa.arrivillaga 在这里写的所有内容:
Class decorator for methods from other class。我要感谢他们的帮助。
让foo() 成为类Foobar 中的一个方法,并让foo() 用MyDecoratorClass() 实例装饰。所以问题是:
运行在foo() 中的代码能否访问MyDecoratorClass()-instance 中的变量?
为了使其正常工作,我们首先需要考虑在程序过程中创建了多少 MyDecoratorClass() 实例。经过@AlexHall 和@juanpa.arrivillaga 的大量研究和帮助,我得出的结论是基本上有三种选择。让我们先快速浏览一下,然后逐一深入研究。
概述
选项 1
一个MyDecoratorClass()-instance 在程序的最开始为(未绑定的)foo() 方法生成,它是唯一用于调用foo() 的实例。每次调用foo(),这个MyDecoratorClass()-instance通过一个trick在方法中插入对应的Foobar()实例。
这种方法允许在foo() 和MyDecoratorClass() 实例中运行的代码之间进行通信。但是,如果您的程序中有多个 Foobar()-instances f1 和 f2,那么 f1.foo() 会影响 f2.foo() 的行为方式 - 因为它们共享相同的 MyDecoratorClass()-instance !
选项 2
对于(未绑定的)foo() 方法,在程序的最开始处再次生成一个 MyDecoratorClass()-instance。但是,每次访问它时,它都会即时返回一个新的MyDecoratorClass()-instance。这个实例是短暂的。它在完成方法后立即死亡。
这种方法不允许在foo() 和MyDecoratorClass()-instance 中运行的代码之间进行任何通信。想象一下,您在内部 foo() 代码并尝试从MyDecoratorClass()-instance 访问变量:
@MyDecoratorClass
def foo(self):
# I want to access the 'decorator_var' right here:
value = self.foo.decorator_var
print(f"foo decorator_var = {value}")
在您尝试访问decorator_var 的那一刻,您实际上会从__get__() 方法返回一个新的MyDecoratorClass()-instance!
选项 3
就像以前一样,一个MyDecoratorClass()-instance 在程序的一开始就为(未绑定的)foo() 方法生成。每次您访问它时(这意味着调用它的__get__() 方法),它都会检查谁 正在尝试访问。如果它是未知的Foobar()-object,则__get__() 方法返回一个带有绑定foo()-方法的新MyDecoratorClass()-instance。如果它是一个已知的Foobar()-object,__get__() 方法会检索它之前为该 Foobar()-object 生成的 MyDecoratorClass()-instance,并返回它。
这个选项确保了一对一的关系:每个Foobar()-object 都得到一个MyDecoratorClass()-instance 来包装它的foo() 方法。并且每个MyDecoratorClass()-instance 恰好属于一个Foobar()-object(*)。非常整洁!
(*) 在程序最开始处为未绑定的foo() 方法生成的MyDecoratorClass()-instance 是这里唯一的例外。但是此实例仅用于其__get__() 方法,该方法用作MyDecoratorClass()-instance-factory:生成、返回和存储一个MyDecoratorClass()-instance,每个Foobar() 实例在其上调用foo() .
让我们来看看每个选项。在此之前,我想强调一下,这三个选项之间唯一的实现区别在于__get__() 方法!
1。第一个选项:坚持一个实例
让MyDecoratorClass 成为类Foobar 中定义的方法foo 的装饰器:
import functools, types
class MyDecoratorClass:
def __init__(self, method) -> None:
functools.update_wrapper(self, method)
self.method = method
def __get__(self, obj, objtype) -> object:
return lambda *args, **kwargs: self.__call__(obj, *args, **kwargs)
def __call__(self, *args, **kwargs) -> object:
return self.method(*args, **kwargs)
class Foobar:
def __init__(self):
pass
@MyDecoratorClass
def foo(self):
print(f"foo!")
即使您从未实例化Foobar(),Python 解释器仍会在您程序的最开始创建一个MyDecoratorClass 实例。这个实例是为 UNBOUND 方法foo() 创建的。选项 1 基本上意味着在程序的其余部分坚持这个MyDecoratorClass()-instance。为此,我们需要确保__get__() 方法不会重新实例化MyDecoratorClass()。相反,它应该使现有的MyDecoratorClass() APPEAR 持有绑定方法:
┌────────────────────────────────────────────────────────────────────────┐
│ def __get__(self, obj, objtype=None): │
│ return lambda *args, **kwargs: self.__call__(obj, *args, **kwargs) │
└────────────────────────────────────────────────────────────────────────┘
如您所见,self.method 永远不会绑定到 Foobar()-instance。相反,它只是这样出现。让我们做一个测试来证明这一点。实例化Foobar() 并调用foo() 方法:
>>> f = Foobar()
>>> f.foo()
方法调用本质上分为两部分:
第 1 部分
f.foo 调用 __get__() 方法。这将在唯一的MyDecoratorClass() 实例上调用,该实例在self.method 中拥有一个未绑定的方法。然后它返回对其__call__() 方法的lambda 引用,但将Foobar() 实例添加到*args 元组中。
第 2 部分
f.foo 之后的括号 '()' 应用于返回的任何 __get__()。在这种情况下,我们知道 __get__() 从 ONE AND ONLY MyDecoratorClass() 实例返回了 __call__() 方法(实际上是用 lambda 进行了一些修改),所以很自然地会调用该方法。
在 __call__() 方法中,我们调用存储的方法(原始 foo),如下所示:
self.method(*args, **kwargs)
虽然self.method 是foo() 的未绑定版本,但Foobar() 实例就在*args 的第一个元素中!
简而言之:每次您在 Foobar()-instance 上调用 foo() 方法时,您都会处理一个且唯一的 MyDecoratorClass()-instance,它包含未绑定的 foo() 方法引用并使其看起来绑定到您在foo() 上调用的Foobar()-instance!
一些额外的测试
您可以使用以下方法验证 self.method 在 __call__() 方法中始终未绑定:
hasattr(self.method, '__self__')
self.method.__self__ is not None
总是打印False!
您还可以在__init__() 方法中添加一个打印语句来验证MyDecoratorClass() 是否只被实例化一次,即使您在多个Foobar() 对象上调用foo()。
注意事项
正如@AlexHall 指出的那样:
return lambda *args, **kwargs: self.__call__(obj, *args, **kwargs)
本质上等同于:
return lambda *args, **kwargs: self(obj, *args, **kwargs)
这是因为在对象上应用括号 '()' 本质上与调用其 __call__() 方法相同。您还可以将 return 语句替换为:
return functools.partial(self, obj)
甚至:
return types.MethodType(self, obj)
2。第二个选项:每次调用创建一个新实例
在第二个选项中,我们在每次 foo() 调用时实例化一个新的 MyDecoratorClass()-instance:
┌─────────────────────────────────────────────────────────────┐
│ def __get__(self, obj, objtype=None): │
│ return type(self)(self.method.__get__(obj, objtype)) │
└─────────────────────────────────────────────────────────────┘
这个MyDecoratorClass()-instance 非常短暂。我检查了__del__() 方法中的打印语句,它会在 foo() 结束后立即收集垃圾!
如果您在多个 Foobar() 实例上调用 foo(),就会发生这种情况:
>>> f1 = Foobar()
>>> f2 = Foobar()
>>> f1.foo()
>>> f2.foo()
与往常一样,未绑定的foo() 方法的MyDecoratorClass()-instance 在任何Foobar()-object 生成之前生成。它一直存在到程序结束。我们称它为不朽的MyDecoratorClass()-instance。
在您调用 foo() 的那一刻,您创建了一个新的短期 MyDecoratorClass()-instance。请记住,foo() 调用基本上分两步进行:
STEP 1
f1.foo 在不朽的MyDecoratorClass() 上调用__get__() 方法-
实例(此时没有其他实例!)。与选项 1 不同,我们现在生成一个新的 MyDecoratorClass() 并将绑定的 foo() 方法作为参数传递给它。这个新的MyDecoratorClass()-instance 被返回。
第 2 步
f1.foo 之后的括号 '()' 应用于返回的任何 __get__()。
我们知道这是一个新的MyDecoratorClass()-instance,所以括号'()' 调用它的__call__() 方法。在__call__() 方法中,我们仍然得到了这个:
self.method(*args, **kwargs)
然而,这一次,没有 Foobar()-object 隐藏在 args 元组中,但存储的方法现在已绑定 - 所以没有必要!
f1.foo() 完成并且短暂的MyDecoratorClass()-instance 被垃圾回收(您可以使用__del__() 方法中的打印语句对此进行测试)。
现在是f2.foo() 的时间了。随着短暂的MyDecoratorClass()-instance 死亡,它会在不朽的实例上调用__get__() 方法(还有什么?)。在此过程中,会创建一个 NEW 实例并重复循环。
简而言之:每个foo() 调用都以调用不朽MyDecoratorClass() 实例上的__get__() 方法开始。这个对象总是返回一个新的但短暂的MyDecoratorClass()-instance,并带有一个绑定的foo()-方法。它在完成工作后死亡。
3。第三个选项:每个 `Foobar()`-instance 一个 `MyDecoratorClass()`-instance
第三个也是最后一个选项结合了两全其美。它为每个Foobar()-instance 创建一个MyDecoratorClass()-instance。
保留__obj_dict__ 字典作为类变量并像这样实现__get__() 方法:
┌───────────────────────────────────────────────────────────────┐
│ def __get__(self, obj, objtype): │
│ if obj in MyDecoratorClass.__obj_dict__: │
│ # Return existing MyDecoratorClass() instance for │
│ # the given object, and make sure it holds a bound │
│ # method. │
│ m = MyDecoratorClass.__obj_dict__[obj] │
│ assert m.method.__self__ is obj │
│ return m │
│ # Create a new MyDecoratorClass() instance WITH a bound │
│ # method, and store it in the dictionary. │
│ m = type(self)(self.method.__get__(obj, objtype)) │
│ MyDecoratorClass.__obj_dict__[obj] = m │
│ return m │
└───────────────────────────────────────────────────────────────┘
因此,每当foo() 被调用时,__get__() 方法() 会检查 MyDecoratorClass()-instance 是否已经为给定Foobar()-object。如果是,则返回 MyDecoratorClass()-instance。否则,将生成一个新的并存储在类字典 MyDecoratorClass.__obj_dict__() 中。
(*) 注意:这个MyDecoratorClass.__obj_dict__ 是一个类级别的字典,你必须在类定义中自己创建。
(*) 注意:同样在这里,__get__() 方法总是在程序开始时产生的不朽 MyDecoratorClass()-instance 上调用 - 在任何 Foobar()-objects 诞生之前.然而,重要的是__get__() 方法返回。
警告
保留一个__obj_dict__ 来存储所有Foobar()-instances 有一个缺点。他们都不会死。根据情况,这可能是一个巨大的内存泄漏。因此,在应用 OPTION 3 之前,请考虑一个合适的解决方案。
我也相信这种方法不允许递归。待测试。
4. `foo()`中的代码和`MyDecoratorClass()`-instance之间的数据交换
让我们回到最初的问题:
让foo() 成为类Foobar 中的一个方法,并让foo() 用MyDecoratorClass()-instance 进行装饰。在foo() 中运行的代码能否访问MyDecoratorClass()-instance 中的变量?
如果您实现了first 或第三个选项,您可以从foo() 代码中访问任何MyDecoratorClass()-instance 变量:
@MyDecoratorClass
def foo(self):
value = self.foo.decorator_var
print(f"foo decorator_var = {value}")
使用self.foo 实际访问MyDecoratorClass()-实例。毕竟MyDecoratorClass() 是self.foo 的包装器!
现在,如果您实施 选项 1,您需要记住 decorator_var 在所有 Foobar() 对象之间共享。对于选项 3,每个 Foobar()-object 都有自己的 MyDecoratorClass() 用于 foo() 方法。
5. 更进一步:在多个方法上应用`@MyDecoratorClass`
选项 3 运行良好 - 直到我在两种方法上应用 @MyDecoratorClass:
class Foobar:
def __init__(self):
pass
@MyDecoratorClass
def foo(self):
print(f"foo!")
@MyDecoratorClass
def bar(self):
print("bar!")
现在试试这个:
>>> f = Foobar()
>>> f.foo()
>>> f.bar()
foo!
foo!
一旦存在Foobar() 对象的MyDecoratorClass()-instance,您将始终访问这个现有的实例来调用该方法。在我们的例子中,这个MyDecoratorClass()-instance 绑定到foo() 方法,所以bar() 永远不会执行!
解决方案是修改我们在__obj_dict__ 中存储MyDecoratorClass()-instance 的方式。不要只为每个Foobar()-object 生成和存储一个MyDecoratorClass()-instance,而是每个(Foobar(),method)组合一个实例!这需要我们的装饰器额外的参数,例如:
@MyDecoratorClass("foo")
def foo(self):
print(f"foo!")
@MyDecoratorClass("bar")
def bar(self):
print("bar!")
带参数的装饰器本质上意味着对底层方法/函数进行双重包装!所以让我们为此设计一个包装器:
def my_wrapper(name="unknown"):
def _my_wrapper_(method):
return MyDecoratorClass(method, name)
return _my_wrapper_
现在使用这个包装器:
class Foobar:
def __init__(self):
pass
@my_wrapper("foo")
def foo(self):
print(f"foo!")
@my_wrapper("bar")
def bar(self):
print("bar!")
最后,我们需要重构MyDecoratorClass:
import functools, types
class MyDecoratorClass:
__obj_dict__ = {}
def __init__(self, method, name="unknown") -> None:
functools.update_wrapper(self, method)
self.method = method
self.method_name = name
return
def __get__(self, obj, objtype) -> object:
if obj in MyDecoratorClass.__obj_dict__.keys():
# Return existing MyDecoratorClass() instance for
# the given object-method_name combination, and make
# sure it holds a bound method.
if self.method_name in MyDecoratorClass.__obj_dict__[obj].keys():
m = MyDecoratorClass.__obj_dict__[obj][self.method_name]
return m
else:
# Create a new MyDecoratorClass() instance WITH a bound
# method, and store it in the dictionary.
m = type(self)(self.method.__get__(obj, objtype), self.method_name)
MyDecoratorClass.__obj_dict__[obj][self.method_name] = m
return m
# Create a new MyDecoratorClass() instance WITH a bound
# method, and store it in the dictionary.
m = type(self)(self.method.__get__(obj, objtype), self.method_name)
MyDecoratorClass.__obj_dict__[obj] = {}
MyDecoratorClass.__obj_dict__[obj][self.method_name] = m
return m
def __call__(self, *args, **kwargs) -> object:
return self.method(*args, **kwargs)
def __del__(self):
print(f"{id(self)} garbage collected!")
让我们修改一下:在程序开始时,在任何Foobar()-object 诞生之前,Python 解释器已经产生了两个MyDecoratorClass()-instances:一个用于未绑定的foo(),另一个用于未绑定的bar()方法。这些是我们不朽的MyDecoratorClass()-instances,其__get__() 方法充当MyDecoratorClass() 工厂。
这里没有什么新鲜事。这也发生在我们进行这些更改之前。但是,现在我们在工厂建成时存储method_name!这样,工厂方法__get__() 可以利用该信息来生成和存储每个Foobar() 对象不仅有一个MyDecoratorClass()-instances,还可以为(Foobar(), "foo") 和(@987654542) 存储一个@,"bar") 组合!
这是完整的独立程序:
import functools, types
class MyDecoratorClass:
__obj_dict__ = {}
def __init__(self, method, name="unknown") -> None:
functools.update_wrapper(self, method)
self.method = method
self.method_name = name
return
def __get__(self, obj, objtype) -> object:
if obj in MyDecoratorClass.__obj_dict__.keys():
# Return existing MyDecoratorClass() instance for
# the given object-method_name combination, and make
# sure it holds a bound method.
if self.method_name in MyDecoratorClass.__obj_dict__[obj].keys():
m = MyDecoratorClass.__obj_dict__[obj][self.method_name]
return m
else:
# Create a new MyDecoratorClass() instance WITH a bound
# method, and store it in the dictionary.
m = type(self)(self.method.__get__(obj, objtype), self.method_name)
MyDecoratorClass.__obj_dict__[obj][self.method_name] = m
return m
# Create a new MyDecoratorClass() instance WITH a bound
# method, and store it in the dictionary.
m = type(self)(self.method.__get__(obj, objtype), self.method_name)
MyDecoratorClass.__obj_dict__[obj] = {}
MyDecoratorClass.__obj_dict__[obj][self.method_name] = m
return m
def __call__(self, *args, **kwargs) -> object:
return self.method(*args, **kwargs)
def __del__(self):
print(f"{id(self)} garbage collected!")
def my_wrapper(name="unknown"):
def _my_wrapper_(method):
return MyDecoratorClass(method, name)
return _my_wrapper_
class Foobar:
def __init__(self):
pass
@my_wrapper("foo")
def foo(self):
print(f"foo!")
@my_wrapper("bar")
def bar(self):
print("bar!")