簡體   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