繁体   English   中英

如何使用 arguments 作为 class 实现 Python 装饰器?

[英]How to implement Python decorator with arguments as a class?

我正在尝试实现一个接受一些 arguments 的装饰器。 通常带有 arguments 的装饰器被实现为双嵌套闭包,如下所示:

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)

这个想法很简单:在实例化时我们只捕获装饰器参数,在调用时我们捕获装饰器 function 并返回装饰器实例的方法,这是绑定的。 绑定很重要,因为在装饰器调用时,我们希望使用存储在其中的所有信息来访问self

然后我们在一些class上使用它:

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

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

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

笔记

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

更新

终于找到解决方案了,看我自己的答案。

这个很有趣。 感谢您发布这个问题。

编写一个不需要 arguments 的简单装饰器非常容易,但是将其扩展到 class 然后被调用 3 次则更具挑战性。 我选择使用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)

如您所见,我选择了一种带有钩子的设计,用于修改 arguments 和方法中的响应。 希望在大多数情况下,这将避免需要触摸__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__中,我们只是将装饰器 arguments 捕获到它的私有属性中。

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

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

    return self

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

描述符是具有“绑定行为”的 object 属性

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

让我们看看细节。

def __get__(self, instance, owner):

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

    if instance is None:
        return self

这是约定俗成的。 当在 class 而不是实例上访问描述符时,它应该返回自己。

下一部分是最有趣的。

        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定义 function(即使我们可以,Python 也无法理解我们),所以我们用闭包封闭装饰器的self - 一个内部 ZC1C425268E68385D1AB5074C1。 在这个闭包中,我们实际上改变了装饰方法( print('decorated: arg. {self._arg}') )的行为,然后调用原始方法。 同样,由于已经有名为self的参数,我们需要为实例的self选择另一个名称——在这个例子中,我将它命名为self_ ,但实际上它是self' ——“自素数”(有点数学幽默)。

        return _decorator.__get__(instance, owner)

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

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 function 使它成为一种方法的原因是binding 绑定是将对象的方法与作为第一个参数隐式传递的实例链接的行为。 按照惯例,它被称为self ,但粗略地说这不是必需的。 您甚至可以将方法存储在其他变量中并调用它,并且仍然可以引用实例:

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

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

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

让我们看看它的类型:

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

哇! 它居然连一堂课! 让我们创造它!

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 中方法的实际工作方式——它们只是在作为实例属性访问时将自身绑定到实例的描述符

为了支持方法调用,函数包括__get__()方法,用于在属性访问期间绑定方法。

所以,我们只需要将绑定机制委托给 function 本身:

_decorator.__get__(instance, owner)

并获得正确绑定的方法!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM