[英]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.