python 在嵌套类函数中只调用一次装饰器

hlswsv35  于 2023-04-04  发布在  Python
关注(0)|答案(3)|浏览(210)

我的类有一个“public”函数,并附加了一个装饰器。在其中一个函数中,我调用了另一个类函数,它也有相同的装饰器。但是由于装饰器已经被调用了,我想跳过这个装饰器调用。有什么方法可以实现吗?
背景:我有一些虚拟的“断路器”,我需要在调用之间打开/关闭它们。但是如果我调用一个已经接触过断路器一次的函数,那么我希望在嵌套调用中避免它。

class foo:

    def my_decorator(func):

        @wraps(func)
        def _wrapper(self, *args, **kwargs):
            print("before")
            val = func(self, *args, **kwargs)
            print("after")
            return val
        return _wrapper

    @my_decorator
    def bar(self):
        return 0

    @my_decorator
    def baz(self):
        self.bar()
        return 1

在这个例子中,我看到:

f.baz()
before
before
after
after

我如何修改它,使我只beforeafter一次,就像bar一样:

f.bar()
before
after
5jvtdoz2

5jvtdoz21#

functools.wraps被用来修饰一个 Package 器函数时,它会将原来的函数(被 Package 器修饰的函数)作为__wrapped__属性附加到 Package 器上。当修饰器被应用到类中时,这些都是属于类的普通函数;这样访问它不会直接在类中找到它,所以它仍然是一个普通的函数而不是一个方法。当然,我们可以显式地传递self给它。
因此:

class foo:
    def my_decorator(func):
        @wraps(func)
        def _wrapper(self, *args, **kwargs):
            print("before")
            val = func(self, *args, **kwargs)
            print("after")
            return val
        return _wrapper
    @my_decorator
    def bar(self):
        return 0
    @my_decorator
    def baz(self):
        foo.bar.__wrapped__(self)
        return 1
vyswwuz2

vyswwuz22#

我的类有一个“public”函数,并附加了一个装饰器。在其中一个函数中,我调用了另一个类函数,它也有相同的装饰器。但是由于装饰器已经被调用了,我想跳过这个装饰器调用。有什么方法可以实现吗?
所以-首先-保持简单的事情简单:如果这是一个一次性的情况,并且您可以在baz中硬编码您想要调用bar而不使用装饰器,您可以简单地显式调用在其__wrapped__属性中给定的函数,就像Karl的答案一样。唯一的客观缺点是它不能与其他装饰器一起工作。
然而,这种方法需要在开发时知道并记住哪些其他方法是用我们只想运行一次的装饰器装饰的,并雅阁它编写内部代码。当使用装饰器时,更标准的做法是,一旦将装饰器作为修饰符添加到函数或方法中,您不必再为此担心-包括不必修改代码来与装饰器协作。
然后,有没有一种方法,在装饰器代码中修改装饰函数,以便在调用另一个装饰方法时跳过装饰器?
不,这不是一个可行和明智的方法,尽管如此-要避免进入装饰器,可能只能通过修改装饰函数中的字节码来完成,以便它可以测试何时调用兄弟装饰函数并调用 Package 函数。
但是一旦进入装饰器,测试它就变得容易了一点--然后你就可以避免重新运行你不想运行两次(或更多次)的代码--这并不是微不足道的,但它确实是一种可靠的可行方法。
首先,你的装饰器需要共享一个状态,这样它就知道什么时候它“重新进入”并跳过相关的代码。一个很好的共享方法可能是通过Python的context-vars:这些是在基于异步的代码中对于线程或任务调用堆栈唯一的状态。
然而,如果你的代码可能涉及到调用同一个对象的其他示例中的方法,并且装饰器代码需要在每个示例中运行一次,你可能需要更多的状态保持。我不会在下面的概念证明中处理这种情况。
所以第一个,冗长但安全的方法是这样的:

import contextvars

from functools import wraps

deco_state = contextvars.ContextVar("deco_state")

def my_decorator(func):

    @wraps(func)
    def _wrapper(self, *args, **kwargs):
        try:
            deco_state.set(depth:=(deco_state.get(0) + 1))
            if depth <= 1:
                print("before")
            val = func(self, *args, **kwargs)
            if depth <= 1:
                print("after")
        finally:
            deco_state.set(deco_state.get() - 1)
        return val
    return _wrapper

class Foo:

    @my_decorator
    def bar(self):
        return 0

    @my_decorator
    def baz(self):
        self.bar()
        return 1

Foo().baz()

如前所述:这将对多线程和多异步任务有弹性-但只有当不同的任务调用同一示例上的方法时。考虑到这种设计,我假设您希望装饰器代码在Foo类的每个示例中运行一次,并且只运行一次,而不管方法是从异步代码中的不同线程或不同任务调用的。
然后,不使用context-vars,只需将调用计数器键入示例即可。
此外,上面的例子很麻烦,需要两个if语句并修改装饰器代码本身。我们可以考虑返工的需要,并创建一个可以装饰你的 Package 器的类(装饰器的装饰器),并被调用来代替functools.wraps装饰器:然后,它可以决定是运行 Package 器还是直接调用修饰函数。
所以这个类还扮演了外部装饰器代码的角色,它只是在“ Package 器”函数的闭包中注解func,这是真实的的装饰器。
当被调用时,这个类提供的 Package 器将知道何时通过你的装饰器运行原始函数,或者直接转到原始函数,并使用contextvars.Context来检查递归性。Contextvars在Python中是一个相对较新的东西,它们可以跟踪“调用堆栈”,即如果一个线程在一个上下文中调用“baz”,另一个线程可以在其他上下文中调用相同的函数,和那些方法(或装饰器方法)中的代码,可以在ContextVar示例中看到不同的值。在这种情况下,我们只需要一个布尔值来知道装饰器是否在给定的调用堆栈中运行。
我花了一些时间来制作这个产品质量,因为这个代码也可以解决我遇到过不止一次的问题:如何避免装饰器运行两次,如你所愿,但当一个 * 覆盖 * 子类中的装饰方法时。
对于这段代码,我们所需要做的就是用相同的装饰器装饰超类中的方法和子类中的重写方法。
总而言之,这是总的代码,您的示例修改为在最后运行:

from functools import wraps
from threading import RLock
import contextvars

RUNONCE = contextvars.ContextVar("RUNONCE", default=None)

class RunOnceDecorator:

    def __init__(self, decorator):
        self.decorator = decorator

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            v = RUNONCE.get()
            try:
                if v is None:
                    def mid():
                        # intermediary function needed to set a state on contextvars.context
                        nonlocal result
                        RUNONCE.set(True)
                        result = self.decorator(func,*args, **kwargs)
                    ctx = contextvars.Context()
                    ctx.run(mid)
                else:
                    ctx = None
                    result = func(*args, **kwargs)
            finally:
                del ctx
            return result
        return wrapper

# so, this is the thing: we can supress the outermost
# function, and get `func` as the very first
# parameter whenever it is called:

@RunOnceDecorator
def my_decorator(func, self, *args, **kwargs):
    print("before")
    val = func(self, *args, **kwargs)
    print("after")
    return val

class Foo:

    @my_decorator
    def bar(self):
        return 0

    @my_decorator
    def baz(self):
        self.bar()
        return 1

Foo().baz()

这将简单地输出:

$ python blip2.py 
before
after

这里没有测试,但它应该可以在多个示例和多线程下工作。

0vvn1miw

0vvn1miw3#

你可以给对象添加一个boolean成员,这里是decorated,它在第一次调用my_decorator时被翻转,在最外面的调用退出之前被翻转。你确实多次进入装饰器函数,但是只要你更关心装饰器函数只执行一次特定的操作(在这个例子中是打印),而不是避免意外的递归,它就应该这样做。

from functools import wraps

class foo:
    decorated = False
    def my_decorator(func):
        @wraps(func)
        def _wrapper(self, *args, **kwargs):
            if not self.decorated:
                print("before")
                self.decorated = True
                val = func(self, *args, **kwargs)
                print("after")
                self.decorated = False
            else:
                val = func(self, *args, **kwargs)
            return val
        return _wrapper

    @my_decorator
    def bar(self):
        return 0

    @my_decorator
    def baz(self):
        self.bar()
        return 1

根据注解,这可能是一个“xy问题”,但是foo.baz在这段代码中只打印beforeafter一次。

相关问题