如何从类装饰器应用的方法中访问变量?

2024-06-16 10:43:59 发布

您现在位置:Python中文网/ 问答频道 /正文

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



我正在编写一个类装饰器来应用于方法。这种做法非常罕见,但幸运的是,StackOverflow社区帮助完成了这项工作: Class decorator for methods from other class

现在我想把事情再向前推进一步。被调用的方法应该可以访问类装饰器中的一些变量。下面是我尝试过的一个独立的小例子:

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()-实例,该实例的decorator_var已初始化为None

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


Tags: 方法selfnonegetreturnfoovaluevar
1条回答
网友
1楼 · 发布于 2024-06-16 10:43:59

这个答案基于@AlexHall和@juanpa.arrivillaga在这里所写的一切: Class decorator for methods from other class。我要感谢他们的帮助。



foo()是类Foobar中的一个方法,让foo()MyDecoratorClass()实例修饰。因此,问题是:

Can the code running in foo() access variables from the MyDecoratorClass()-instance?

为了使其正常工作,我们需要首先考虑在程序过程中创建了多少MyDecoratorClass()实例。经过@AlexHall和@juanpa.arrivillaga的大量研究和帮助,我得出结论,基本上有三种选择。让我们先快速浏览一下,然后逐一深入调查

概述

OPTION 1
One MyDecoratorClass()-instance spawns at the very beginning of your program for the (unbound) foo() method, and it's the only instance used to invoke foo(). Each time you invoke foo(), this MyDecoratorClass()-instance inserts the corresponding Foobar() instance in the method through a trick.

这种方法允许在foo()MyDecoratorClass()-实例中运行的代码之间进行通信。但是,如果您的程序中有多个Foobar()实例f1f2,那么f1.foo()可能会影响f2.foo()的行为方式,因为它们共享相同的MyDecoratorClass()实例

OPTION 2
Again one MyDecoratorClass()-instance spawns at the very beginning of the program for the (unbound) foo() method. However, each time you access it, it returns a NEW MyDecoratorClass()-instance on the fly. This instance is short-lived. It dies immediately after completing the method.

这种方法不允许在foo()中运行的代码与MyDecoratorClass()-实例之间进行任何通信。假设您在foo()代码中,试图从MyDecoratorClass()实例访问变量:

@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()实例

OPTION 3
Just like before, one MyDecoratorClass()-instance spawns at the very beginning of the program for the (unbound) foo() method. Each time you access it (which implies calling its __get__() method), it checks who is trying to access. If it's an unknown Foobar()-object, the __get__() method returns a NEW MyDecoratorClass()-instance with a bound foo()-method. If it's a known Foobar()-object, the __get__() method retrieves the MyDecoratorClass()-instance it has spawn before for that very Foobar()-object, and returns it.

此选项确保了一对一的关系:每个Foobar()-对象只获得一个MyDecoratorClass()-实例来包装其foo()方法。并且每个MyDecoratorClass()实例恰好属于一个Foobar()-对象(*)。非常整洁

(*)程序开始时为unboundfoo()方法生成的MyDecoratorClass()实例是这里唯一的例外。但是这个实例只用于它的__get__()方法,该方法充当MyDecoratorClass()实例工厂:在调用foo()的每个Foobar()实例上生成、返回和存储一个MyDecoratorClass()实例。

让我们看一下每个选项。在这样做之前,我想强调的是,这三个选项之间的唯一实现差异在于__get__()方法



一,。第一个选项:坚持使用一个实例

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()实例。为了实现这一点,我们需要确保__get__()方法不会重新实例化MyDecoratorClass()。相反,它应该使现有的MyDecoratorClass()看起来包含绑定方法:

    ┌────────────────────────────────────────────────────────────────────────┐
    │ def __get__(self, obj, objtype=None):                                  │
    │     return lambda *args, **kwargs: self.__call__(obj, *args, **kwargs) │
    └────────────────────────────────────────────────────────────────────────┘

如您所见,self.method从不绑定到Foobar()-实例。相反,它只是以这种方式出现。让我们做一个测试来证明这一点。实例化Foobar()并调用foo()方法:

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

方法调用基本上由两部分组成:

PART 1
f.foo invokes the __get__() method. This gets invoked on the ONE AND ONLY MyDecoratorClass() instance, which holds an unbound method in self.method. It then returns a lambda-reference to its __call__() method, but with the Foobar() instance added to the *args tuple.

PART 2
The parenthesis '()' after f.foo are applied on WHATEVER __get__() returned. In this case, we know that __get__() returned the __call__() method from the ONE AND ONLY MyDecoratorClass() instance (actually a bit modified with lambda), so naturally that method gets invoked.

Inside the __call__() method, we invoke the stored method (the original foo) like so:

self.method(*args, **kwargs)

While self.method is an unbound version of foo(), the Foobar() instance is right there in the first element of *args!

简而言之:每次在Foobar()实例上调用foo()方法时,您都要处理一个也是唯一一个MyDecoratorClass()实例,该实例包含一个未绑定的foo()方法引用,并使它看起来绑定到您在foo()实例上调用的Foobar()

一些额外的测试
您可以使用以下方法验证self.method__call__()方法中始终未绑定self.method

  • hasattr(self.method, '__self__')
  • self.method.__self__ is not None

总是打印False

您还可以在__init__()方法中放置一个print语句,以验证MyDecoratorClass()仅实例化一次,即使您在多个Foobar()对象上调用foo()

注释
正如@AlexHall指出的,这:

return lambda *args, **kwargs: self.__call__(obj, *args, **kwargs)

基本上与以下内容相同:

return lambda *args, **kwargs: self(obj, *args, **kwargs)

这是因为在对象上应用括号'()'与调用其__call__()方法基本相同。你也可以将返回语句替换为:

return functools.partial(self, obj)

甚至:

return types.MethodType(self, obj)


二,。第二个选项:每次调用创建一个新实例

在第二个选项中,我们在每次foo()调用时实例化一个新的MyDecoratorClass()实例:

    ┌─────────────────────────────────────────────────────────────┐
    │ def __get__(self, obj, objtype=None):                       │
    │     return type(self)(self.method.__get__(obj, objtype))    │
    └─────────────────────────────────────────────────────────────┘

这个MyDecoratorClass()实例非常短暂。我已经用__del__()方法中的print语句检查过,它在foo()结束后立即被垃圾收集

因此,如果在多个Foobar()实例上调用foo(),就会发生这种情况:

>>> f1 = Foobar()
>>> f2 = Foobar()
>>> f1.foo()
>>> f2.foo()

与往常一样,unbound foo()方法的MyDecoratorClass()实例在任何Foobar()对象生成之前都会生成。它将一直保持活动状态,直到程序结束。让我们称之为不朽MyDecoratorClass()-实例

在调用foo()时,就创建了一个新的短期MyDecoratorClass()-实例。请记住,foo()调用基本上分两步进行:

STEP 1
f1.foo invokes the __get__() method on the immortal MyDecoratorClass()- instance (there is no other at this point!). Unlike OPTION 1, we now spawn a NEW MyDecoratorClass() and pass it a bound foo() method as argument. This new MyDecoratorClass()-instance gets returned.

STEP 2
The parenthesis '()' after f1.foo are applied on WHATEVER __get__() returned. We know it's a NEW MyDecoratorClass()-instance, so the parenthesis '()' invoke its __call__() method. Inside the __call__() method, we still got this:

self.method(*args, **kwargs)

This time however, there is NO Foobar()-object hidden in the args tuple, but the stored method is bound now - so there is no need for that!

f1.foo()完成,短寿命的MyDecoratorClass()-实例被垃圾回收(您可以使用__del__()方法中的print语句来测试这一点)

现在是f2.foo()的时间了。当短命的MyDecoratorClass()实例死亡时,它调用不朽实例上的__get__()方法(还有什么?)。在此过程中,将创建一个新实例,并重复该循环

简言之:每个foo()调用都从对不朽MyDecoratorClass()实例调用__get__()方法开始。此对象始终返回一个新的但短暂的MyDecoratorClass()实例,该实例带有一个绑定的foo()方法。它在完成工作后死亡



三,。第三个选项:每个'Foobar()`-实例一个'mydecorclass()`-实例

第三个也是最后一个选项结合了这两个方面的优点。它为每个Foobar()实例创建一个MyDecoratorClass()实例

保持__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()对象的MyDecoratorClass()实例是否已经生成(使用绑定方法)。如果是,则返回MyDecoratorClass()-实例。否则,将生成一个新的类并将其存储在类字典MyDecoratorClass.__obj_dict__()

(*)注意:这个MyDecoratorClass.__obj_dict__是一个类级字典,您必须在类定义中自己创建。

(*)注意:在这里,__get__()方法总是在任何Foobar()对象生成之前,在程序最开始生成的不朽MyDecoratorClass()实例上调用。然而,重要的是__get__()方法返回的内容

警告
保留一个__obj_dict__来存储所有Foobar()-实例有一个缺点。他们都不会死。根据具体情况,这可能是一个巨大的内存泄漏。因此,在应用选项3之前,请考虑一个合适的解决方案

我也相信这种方法不允许递归。有待检验



四,。`foo()`中的代码与`MyDecoratorClass()`-实例之间的数据交换

让我们回到最初的问题:

Let foo() be a method from class Foobar, and let foo() be decorated with a MyDecoratorClass()-instance. Can the code running in foo() access variables from the MyDecoratorClass()-instance?

如果实现第一个第三个选项,则可以从foo()代码中访问任何MyDecoratorClass()实例变量:

@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,对于foo()方法,每个Foobar()-对象都有自己的MyDecoratorClass()



五,。更进一步:在几个方法上应用“@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()-实例,您将始终访问这个现有实例来调用该方法。在我们的例子中,这个MyDecoratorClass()-实例绑定到foo()方法,因此bar()永远不会执行

解决方案是修改MyDecoratorClass()实例在__obj_dict__中的存储方式。不要只为每个Foobar()对象生成和存储一个MyDecoratorClass()实例,而是为每个(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()对象生成之前,Python解释器已经生成了两个MyDecoratorClass()实例:一个用于unboundfoo(),另一个用于unboundbar()方法。这些是我们不朽的MyDecoratorClass()实例,它们的__get__()方法充当MyDecoratorClass()工厂

这里没什么新鲜事。这在我们做这些改变之前也发生过。然而,现在我们在工厂建成时存储method_name!通过这种方式,工厂方法__get__()可以利用这些信息,不仅为每个Foobar()对象生成和存储一个MyDecoratorClass()实例,而且为(Foobar()"foo")和(Foobar()"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!")


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!")

相关问题 更多 >