【问题标题】:How to access variables from a Class Decorator from within the method it's applied on?如何从应用类装饰器的方法中访问变量?
【发布时间】:2020-12-04 13:06:00
【问题描述】:

注意
我根据@AlexHall 和@juanpa.arrivillaga 编写的所有内容编写了一个答案。见下文。



我正在编写一个 Class Decorator 以应用于方法。这种做法并不常见,但幸运的是 StackOverflow 社区帮助完成了它: Class decorator for methods from other class

现在我想更进一步。被调用的方法应该可以访问 Class Decorator 中的一些变量。这是我尝试过的一个独立的小示例:

import functools

class MyDecoratorClass:
    def __init__(self, method) -> None:
        functools.update_wrapper(self, method)
        self.method = method
        self.decorator_var = None
        return

    def __get__(self, obj, objtype) -> object:
        return type(self)(self.method.__get__(obj, objtype))

    def __call__(self, *args, **kwargs) -> object:
        self.decorator_var = "hello world"
        retval = self.method(*args, **kwargs)
        return retval

class Foobar:
    def __init__(self):
        pass

    @MyDecoratorClass
    def foo(self):
        # I want to access the 'decorator_var' right here:
        value = self.foo.decorator_var
        print(f"foo decorator_var = {value}")

让我们测试一下:

>>> f = Foobar()
>>> f.foo()
foo decorator_var = None

如您所见,变量decorator_var 未正确访问。我相信这发生在我尝试访问变量时:
value = self.foo.decorator_var

访问self.foo 会调用来自MyDecoratorClass__get__() 方法。这将返回一个新的 MyDecoratorClass()-instance,其 decorator_var 初始化为 None

有没有办法可以从foo() 方法中访问decorator_var

【问题讨论】:

  • 我的意思是,我不确定如何解释它。第一种方法显式创建一个新实例return type(self)(...) 另一种方法没有,return types.MethodType(...)

标签: python python-3.x decorator python-decorators


【解决方案1】:

这个答案基于@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 f1f2,那么 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.methodfoo() 的未绑定版本,但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!")

【讨论】:

    猜你喜欢
    • 2011-10-30
    • 1970-01-01
    • 2018-06-28
    • 2019-12-21
    • 1970-01-01
    • 2020-05-02
    • 2020-05-14
    • 2014-12-19
    • 2011-06-26
    相关资源
    最近更新 更多