[英]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_post
“ self
”和所有 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.