Как реализовать Python декоратор с аргументами в качестве класса? - PullRequest
1 голос
/ 19 сентября 2019

Я пытаюсь реализовать декоратор, который принимает некоторые аргументы.Обычно декораторы с аргументами реализованы в виде двойных вложений, например:

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)

Идея очень проста: во время создания экземпляра мы просто собираем аргументы декоратора, а во время вызова мы фиксируем декорированную функцию и возвращаем метод экземпляра декоратора, который связан.Важно, чтобы он был связан, потому что во время вызова декоратора мы хотим получить доступ к self со всей информацией, хранящейся в нем.

Затем мы используем его для некоторого класса:

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

К сожалениюэтот подход не работает.Проблема в том, что во время вызова декоратора мы теряем контекст декорированного экземпляра, потому что во время декорирования (при определении класса) декорированный метод не привязан.Привязка происходит позже во время доступа к атрибуту.Но в этот момент у нас уже есть метод привязки декоратора (jsonschema_validate._decorator), и self передается неявно, и его значение не является MyResource экземпляром, скорее jsonschema_validate экземпляром.И мы не хотим потерять это self значение, потому что мы хотим получить доступ к его атрибутам во время вызова декоратора.В итоге это приводит к TypeError при вызове self._fn(req, resp, *args, **kwargs) с жалобами на то, что «требуемый позиционный аргумент 'resp' отсутствует", потому что переданный в req arg становится MyResource.on_post "self" и все аргументы фактически "сдвигаются".

Итак, есть ли способ реализовать декоратор как класс, а не как набор вложенных функций?

Примечание

Как моя первая попытка реализовать декоратор как простой классдовольно быстро вышел из строя, я сразу же вернулся к вложенным функциям.Кажется, что правильно реализованный подход к классу еще более нечитабелен и запутан, но я все равно хочу найти решение для удовольствия.

1 Ответ

1 голос
/ 19 сентября 2019

Это весело!Спасибо за публикацию этого вопроса.

Написать простой декоратор, который не принимает аргументов, довольно легко, но расширить его на класс, который затем вызывается трижды, немного сложнее.Я решил использовать 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__.

. Я не мог придумать, как прикрепить params к ParamsDecorator после возврата partial,поэтому мне пришлось выбрать его в SimpleDecorator, но не использовать его.

Я думаю, что это помогает сохранить контент плоским, а не вложенным.Мне также нравится, что это может позаботиться о functools.wraps для вас, поэтому вам не нужно беспокоиться о включении этого в эти декораторы.Недостатком написания декоратора таким образом является то, что вы сейчас вводите новый модуль, который вам необходимо будет установить или поддерживать, а затем импортировать каждый раз, когда пишете декоратор.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...