如何使用 class 参数实现 Python 装饰器?

How to implement Python decorator with arguments as a class?

我正在尝试实现一个接受一些参数的装饰器。通常带参数的装饰器被实现为双嵌套闭包,像这样:

def mydecorator(param1, param2):
    # do something with params
    def wrapper(fn):
        def actual_decorator(actual_func_arg1, actual_func_arg2):
            print("I'm decorated!")

            return fn(actual_func_arg1, actual_func_arg2)

        return actual_decorator

    return wrapper

但我个人不喜欢这种方法,因为它非常难读且难以理解。

所以我得到了这个:

class jsonschema_validate(object):
    def __init__(self, schema):
        self._schema = schema

    def __call__(self, fn):
        self._fn = fn

        return self._decorator

    def _decorator(self, req, resp, *args, **kwargs):
        try:
            jsonschema.validate(req.media, self._schema, format_checker=jsonschema.FormatChecker())
        except jsonschema.ValidationError as e:
            _log.exception('Validation failed: %r', e)

            raise errors.HTTPBadRequest('Bad request')

        return self._fn(req, resp, *args, **kwargs)

这个想法很简单:在实例化时我们只捕获装饰器参数,在调用时我们捕获装饰函数和绑定的return装饰器实例的方法。绑定它很重要,因为在装饰器调用时我们想要访问 self 以及存储在其中的所有信息。

然后我们用在一些class:

class MyResource(object):
    @jsonschema_validate(my_resource_schema)
    def on_post(self, req, resp):
        pass

不幸的是,这种方法行不通。问题在于,在装饰器调用时,我们会丢失装饰实例的上下文,因为在装饰时(定义 class 时)装饰方法未绑定。绑定稍后在属性访问时发生。但是此时我们已经有了装饰器的绑定方法(jsonschema_validate._decorator)并且self被隐式传递,它的值不是 MyResource实例,而是 jsonschema_validate 实例。我们 不想松动 这个 self 值,因为我们想在装饰器调用时访问它的属性。最后,它在调用 self._fn(req, resp, *args, **kwargs) 时导致 TypeError 并抱怨“缺少所需的位置参数 'resp'”,因为传入的 req arg 变为 MyResource.on_postself" 并且所有参数有效地“转移”。

那么,有没有办法将装饰器实现为 class 而不是一堆嵌套函数?

备注

由于我第一次尝试将装饰器实现为简单 class 很快就失败了,我立即恢复到嵌套函数。似乎正确实施的 class 方法更加难以理解和纠结,但我还是想找到解决方案,以获得乐趣。

更新

终于找到解决办法了,看我自己的回答。

这很有趣!感谢您提出这个问题。

编写一个简单的不带参数的装饰器非常容易,但是将其扩展到一个 class 然后被调用三次则更具挑战性。我选择使用 functools.partial 来解决这个问题。

from functools import partial, update_wrapper
from unittest import TestCase, main


class SimpleDecorator(object):

    def __new__(cls, func, **params):
        self = super(SimpleDecorator, cls).__new__(cls)
        self.func = func
        self.params = params
        return update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        args, kwargs = self.before(*args, **kwargs)
        return self.after(self.func(*args, **kwargs))

    def after(self, value):
        return value

    def before(self, *args, **kwargs):
        return args, kwargs


class ParamsDecorator(SimpleDecorator):

    def __new__(cls, **params):
        return partial(super(ParamsDecorator, cls).__new__, cls, **params)


class DecoratorTestCase(TestCase):

    def test_simple_decorator(self):
        class TestSimpleDecorator(SimpleDecorator):

            def after(self, value):
                value *= 2
                return super().after(value)

        @TestSimpleDecorator
        def _test_simple_decorator(value):
            """Test simple decorator"""
            return value + 1

        self.assertEqual(_test_simple_decorator.__name__, '_test_simple_decorator')
        self.assertEqual(_test_simple_decorator.__doc__, 'Test simple decorator')
        self.assertEqual(_test_simple_decorator(1), 4)

    def test_params_decorator(self):
        class TestParamsDecorator(ParamsDecorator):

            def before(self, value, **kwargs):
                value *= self.params['factor']
                return super().before(value, **kwargs)

        @TestParamsDecorator(factor=3)
        def _test_params_decorator(value):
            """Test params decorator"""
            return value + 1

        self.assertEqual(_test_params_decorator.__name__, '_test_params_decorator')
        self.assertEqual(_test_params_decorator.__doc__, 'Test params decorator')
        self.assertEqual(_test_params_decorator(2), 7)

如您所见,我选择了一种带有挂钩的设计,用于修改方法中的参数和响应。希望大多数时候这会避免需要触摸 __call____new__.

我想不出在返回 partial 后将 params 附加到 ParamsDecorator 的方法,所以我不得不选择将其放入 SimpleDecorator但没有使用它。

我认为这可以很好地保持内容扁平而不是嵌套。我也喜欢它可以为您处理 functools.wraps,因此您不必担心将其包含在这些装饰器中。以这种方式编写装饰器的缺点是您现在引入了一个新模块,您需要安装或维护它,然后在每次编写装饰器时导入它。

终于明白了!

正如我所写,一个方法不能有两个的问题self,所以我们需要以某种方式捕获这两个值。 描述符闭包来拯救!

这是完整的例子:

class decorator_with_args(object):
    def __init__(self, arg):
        self._arg = arg

    def __call__(self, fn):
        self._fn = fn

        return self

    def __get__(self, instance, owner):
        if instance is None:
            return self

        def _decorator(self_, *args, **kwargs):
            print(f'decorated! arg: {self._arg}')

            return self._fn(self_, *args, **kwargs)

        return _decorator.__get__(instance, owner)

让我们把它分解成碎片!

它的启动与我之前的尝试完全相同。在 __init__ 中,我们只捕获装饰器参数到它的私有属性。

对下一部分更感兴趣:__call__ 方法。

def __call__(self, fn):
    self._fn = fn

    return self

和以前一样,我们将装饰方法捕获到装饰器的私有属性。但是,我们 return self 而不是 return 实际的装饰器方法(前面示例中的 def _decorator )。所以装饰方法成为装饰器的实例。这是允许它充当 descriptor 所必需的。根据文档:

a descriptor is an object attribute with "binding behavior"

令人困惑,呃?实际上,它比看起来容易。描述符只是一个具有“魔法”(dunder)方法的对象,它被分配给另一个对象的属性。当您尝试访问此属性时,将使用某些调用约定调用那些 dunder 方法。稍后我们将return“绑定行为”。

让我们看看细节。

def __get__(self, instance, owner):

描述符必须至少实现 __get__ dunder(以及 __set__ & __delete__ 可选)。这称为“描述符协议”(类似于“上下文管理器协议”、“收集协议”等)。

    if instance is None:
        return self

这是约定俗成的。当描述符在 class 而不是实例上访问时,它应该 return 本身。

下一部分最有趣。

        def _decorator(self_, *args, **kwargs):
            print(f'decorated! arg: {self._arg}')

            return self._fn(self_, *args, **kwargs)

        return _decorator.__get__(instance, owner)

我们需要以某种方式捕获装饰器的 self 以及装饰实例的 self。由于我们不能用两个self定义函数(即使我们可以,Python也无法理解我们),所以我们封装装饰器的self with closure - 一个内部函数。在这个闭包中,我们实际上改变了装饰方法(print('decorated! arg: {self._arg}'))的行为,然后调用了原来的方法。同样,由于已经有名为 self 的参数,我们需要为实例的 self 选择另一个名称 - 在这个例子中我将其命名为 self_,但实际上它是 self' - “自我素数”(有点数学幽默)。

        return _decorator.__get__(instance, owner)

最后,通常,当我们定义闭包时,我们只是 return 它:def inner(): pass; return inner。但在这里我们不能那样做。因为“绑定行为”。我们需要的是 returned 闭包 绑定到装饰实例 以使其正常工作。让我用一个例子来解释。

class Foo(object):
    def foo(self):
        print(self)

Foo.foo
# <function Foo.foo at 0x7f5b1f56dcb0>
Foo().foo
# <bound method Foo.foo of <__main__.Foo object at 0x7f5b1f586910>>

当您访问 class 上的方法时,它只是一个 普通 Python 函数 。是什么让它成为 方法 而不是 绑定 。绑定是将对象的方法与作为第一个参数隐式传递的实例链接起来的行为。通过convention,它被称为self,但粗略地说这不是必需的。您甚至可以将方法存储在其他变量中并调用它,并且仍然会引用实例:

f = Foo()
f.foo()
# <__main__.Foo object at 0x7f5b1f5868d0>
other_foo = f.foo
other_foo()
# <__main__.Foo object at 0x7f5b1f5868d0>

因此,我们需要将 returned 闭包绑定到装饰实例。怎么做?还记得我们在看方法吗?可能是这样的:

# <bound method Foo.foo of <__main__.Foo object at 0x7f5b1f586910>>

让我们看看它的类型:

type(f.foo)
# <class 'method'>

哇!它竟然连一个class!让我们来创造吧!

method()
# Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
# NameError: name 'method' is not defined

遗憾的是,我们不能直接这样做。但是,有 types.MethodType:

types.MethodType
# <class 'method'>

看来我们终于找到我们想要的了!但是,实际上,我们不需要手动创建方法!。我们需要做的就是委托给方法创建的标准机制。这是 Python 中的 how actually methods work - 它们只是 描述符 ,当作为实例的属性访问时,它们将自己绑定到实例!

To support method calls, functions include the __get__() method for binding methods during attribute access.

所以,我们只需要委托绑定机制来运行它自己:

_decorator.__get__(instance, owner)

并获取正确绑定的方法!