如何在函数上使用的装饰器 class 中获取函数句柄?

How to get the function handle within a decorator class which is used on a function?

以下文件结构位于整个 project 文件夹中的 testing 模块中:

project/
│── testing/
│   ├── __init__.py
│   ├── store.py
│   └── store_functions.py
│
└── main.py

testing/store.py

class Store:
    
    registered = []
    
    def __init__(self, *args, **kwargs)
        Store.registered.append(self)
    
    def __call__(self, func):
        pass
    

testing/store_functions.py

@Store('test-xyz')
def my_test_function(testname):
    pass

基本想法是,我想在某处导入 testing 模块,并且应该自动收集带有 @Store 装饰器的定义函数并存储在 Store.registered 中。 所以里面 main.py:

import testing # this automatically collects all @Store decorators and puts all of them into Store.registered

registered_stores = Store.registered

但我的问题出在 Store.__init__ 函数中,我无法获得 my_test_function 的函数句柄: testing/store.py

class Store:
    
    registered = []
    
    def __init__(self, *args, **kwargs):
        # here I need access to the actual function this decorator was assigned to for examle:
        # @Store('test-xyz')
        # def my_test_function(testname):
        #     pass

        # I would need access to the function handle of my_test_function

这有可能吗? 或者我需要以何种方式更改我的代码才能使 design/approach 正常工作? 非常感谢任何帮助 and/or 提示!

为了解决这个问题,我将定义一个函数来接收参数,例如 'text-xyz' 和 return 装饰器将调用 Store:

def store(*args, **kwargs):
    def decor(func):
        Store(func, *args, **kwargs)
        return func
    return decor

class Store:
    def __init__(self, func, *args, **kwargs):
        # use func, args aand kwargs

用法:

@store('test-xyz')  # note the lower-case store
def my_test_function(testname):
    pass

作为 的替代方案,如果您想将所有装饰器逻辑保留在 Store class 中,则必须以不同方式构建它.

前言:了解接受参数的装饰器

首先,记住装饰器是如何工作的:

@foo
def bar():
    pass

# is equivalent to:

def bar():
    pass
bar = foo(bar)

重要的是要理解 @ 之后的表达式的计算结果是 callable,它接受一个参数,这是它正在装饰的可调用对象;在这种情况下,bar。考虑到这一点,当我们想要创建一个可以接受参数的装饰器时,我们最终会在行为上做出一个微妙但非常重要的改变,比如:

@foo('wowza')
def frobnicate():
    pass

# is equivalent to:

def frobnicate():
    pass
frobnicate = foo('wowza')(frobnicate)

同样,@ 之后的表达式的计算结果应该是可调用的。但是,现在我们的表达式是函数 call 而不是函数 object!因此,调用 foo('wowza')result 被用作装饰器,这意味着 result 将被尝试用作可调用并传递给 frobnicate作为论据。如果结果 可调用的,那就太好了!但它可能不是,所以这样的装饰器必须以不同的方式构造。

创建存储装饰器

这是实现;脱离问题中的示例,您希望能够构造 Storage 带有参数的实例,然后将其存储在实例中。

import functools

class Storage:
    registered = []

    def __init__(self, value=None):
        # Note that we don't have the decorated function object yet. This is
        # intentional! Remember how decorators are evaluated! All we do here is
        # store our decorator arguments.
        self.value = value

        # create any other needed attributes

    # This is our "callable" that takes in the decorated function!
    def __call__(self, func):
        # Using type avoids hard-coding the class name
        type(self).registered.append(func)

        # do something with self.value if needed
        # do something with func if needed

        # The usual functools.wraps call. This preserves the decorated
        # function's name and docstring.
        @functools.wraps(func)

        # This should be familiar; it's just like writing a normal decorator.
        def wrapper(*args, **kwargs):
            # do something with func, func(), and/or self.value if needed
            return func(*args, **kwargs)

        # This function object is what the name of the decorated function is
        # bound to.
        return wrapper

这是一个没有所有评论的版本:

import functools

class Storage:
    registered = []

    def __init__(self, value=None):
        self.value = value

    def __call__(self, func):
        type(self).registered.append(func)
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper

应用装饰器按预期工作:

@Storage('test-xyz')
def my_test_function(testname):
    pass

请注意,如果您希望装饰器参数是可选的,即使您没有传递任何参数,您仍然需要“调用”装饰器;就像您在实例化任何其他 class:

时一样
@Storage()
def my_test_function2(testname):
    pass

如果您想通过大量示例更深入地解释如何创建不同种类的装饰器(包括像这样的装饰器),我强烈推荐 RealPython 的 Primer on Python Decorators。它非常详尽,比我在这里介绍的更详细。