Создание декораторов с дополнительными аргументами - программирование
Подтвердить что ты не робот

Создание декораторов с дополнительными аргументами

from functools import wraps

def foo_register(method_name=None):
    """Does stuff."""
    def decorator(method):
        if method_name is None:
            method.gw_method = method.__name__
        else:
            method.gw_method = method_name
        @wraps(method)
        def wrapper(*args, **kwargs):
            method(*args, **kwargs)
        return wrapper
    return decorator

Пример: следующее украшает my_function с помощью foo_register, а не делает его decorator.

@foo_register
def my_function():
    print('hi...')

Пример: Следующее работает как ожидалось.

@foo_register('say_hi')
def my_function():
    print('hi...')

Если я хочу, чтобы он работал корректно в обоих приложениях (один из них использовал method.__name__ и один передавал имя в), я должен проверить внутри foo_register, чтобы увидеть, является ли первый аргумент декоратором, и если да, Я должен: return decorator(method_name) (вместо return decorator). Такой вид "проверки, чтобы убедиться, что он вызываемый" кажется очень хаки. Есть ли лучший способ создать многопользовательский декоратор, подобный этому?

P.S. Я уже знаю, что я могу потребовать, чтобы декоратор был вызван, но это не "решение". Я хочу, чтобы API чувствовал себя естественным. Моя жена любит украшать, и я не хочу этого испортить.

4b9b3361

Ответ 1

Гленн, я должен был это сделать. Наверное, я рад, что нет "волшебного" способа сделать это. Я их ненавижу.

Итак, вот мой собственный ответ (имена методов отличаются от выше, но одна и та же концепция):

from functools import wraps

def register_gw_method(method_or_name):
    """Cool!"""
    def decorator(method):
        if callable(method_or_name):
            method.gw_method = method.__name__
        else:
            method.gw_method = method_or_name
        @wraps(method)
        def wrapper(*args, **kwargs):
            method(*args, **kwargs)
        return wrapper
    if callable(method_or_name):
        return decorator(method_or_name)
    return decorator

Пример использования (обе версии работают одинаково):

@register_gw_method
def my_function():
    print('hi...')

@register_gw_method('say_hi')
def my_function():
    print('hi...')

Ответ 2

Самым чистым способом, который я знаю для этого, является следующее:

import functools


def decorator(original_function=None, optional_argument1=None, optional_argument2=None, ...):

    def _decorate(function):

        @functools.wraps(function)
        def wrapped_function(*args, **kwargs):
            ...

        return wrapped_function

    if original_function:
        return _decorate(original_function)

    return _decorate

объяснение

Когда декоратор вызывается без необязательных аргументов, подобных этому:

@decorator
def function ...

Функция передается в качестве первого аргумента, и decorate возвращает оформленную функцию, как и ожидалось.

Если декоратор вызывается с одним или несколькими необязательными аргументами вроде этого:

@decorator(optional_argument1='some value')
def function ....

Затем вызывается декоратор с аргументом функции со значением None, поэтому возвращаемая функция возвращается, как и ожидалось.

Python 3

Обратите внимание, что приведенная выше подпись декоратора может быть улучшена с помощью специфичного для Python 3 *, синтаксиса для обеспечения безопасного использования аргументов ключевых слов. Просто замените сигнатуру самой внешней функции на:

def decorator(original_function=None, *, optional_argument1=None, optional_argument2=None, ...):

Ответ 3

С помощью ответов здесь и в другом месте, а также множество проб и ошибок, я обнаружил, что на самом деле существует гораздо более простой и общий способ заставить декораторов принимать дополнительные аргументы. Он проверяет аргументы, с которыми он был вызван, но нет другого способа сделать это.

Ключом к украсить ваш декоратор.

Код декоратора декоратора

Вот декоратор декоратора (этот код является общим и может использоваться любым, кому нужен дополнительный декоратор arg):

def optional_arg_decorator(fn):
    def wrapped_decorator(*args):
        if len(args) == 1 and callable(args[0]):
            return fn(args[0])

        else:
            def real_decorator(decoratee):
                return fn(decoratee, *args)

            return real_decorator

    return wrapped_decorator

Использование

Использование его так же просто, как:

  • Создайте декоратор, как обычно.
  • После первого аргумента целевой функции добавьте необязательные аргументы.
  • Украсьте декоратор optional_arg_decorator

Пример:

@optional_arg_decorator
def example_decorator_with_args(fn, optional_arg = 'Default Value'):
    ...
    return fn

Контрольные примеры

Для вашего варианта использования:

Итак, для вашего случая сохранить атрибут функции с именем метода переданного или __name__, если None:

@optional_arg_decorator
def register_method(fn, method_name = None):
    fn.gw_method = method_name or fn.__name__
    return fn

Добавить декорированные методы

Теперь у вас есть декоратор, который можно использовать с или без args:

@register_method('Custom Name')
def custom_name():
    pass

@register_method
def default_name():
    pass

assert custom_name.gw_method == 'Custom Name'
assert default_name.gw_method == 'default_name'

print 'Test passes :)'

Ответ 4

Как насчет

from functools import wraps, partial

def foo_register(method=None, string=None):
    if not callable(method):
        return partial(foo_register, string=method)
    method.gw_method = string or method.__name__
    @wraps(method)
    def wrapper(*args, **kwargs):
        method(*args, **kwargs)
    return wrapper

Ответ 5

Улучшенный общий код декоратора

Здесь моя адаптация ответа @Nicole со следующими улучшениями:

  • необязательные kwargs могут быть переданы декорированному декоратору
  • декорированный декоратор может быть связанным методом
import functools

def optional_arg_decorator(fn):
    @functools.wraps(fn)
    def wrapped_decorator(*args, **kwargs):
        is_bound_method = hasattr(args[0], fn.__name__) if args else False

        if is_bound_method:
            klass = args[0]
            args = args[1:]

        # If no arguments were passed...
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            if is_bound_method:
                return fn(klass, args[0])
            else:
                return fn(args[0])

        else:
            def real_decorator(decoratee):
                if is_bound_method:
                    return fn(klass, decoratee, *args, **kwargs)
                else:
                    return fn(decoratee, *args, **kwargs)
            return real_decorator
    return wrapped_decorator

Ответ 6

Теперь, когда этот старый поток снова встает наверху, лемми просто вбрасываем некоторые украшения:

def magical_decorator(decorator):
    @wraps(decorator)
    def inner(*args, **kw):
        if len(args) == 1 and not kw and callable(args[0]):
            return decorator()(args[0])
        else:
            return decorator(*args, **kw)
    return inner

Теперь ваш волшебный декоратор находится всего в одной линии!

@magical_decorator
def foo_register(...):
    # bla bla

Кстати, это работает для любого декоратора. Это просто приводит к тому, что @foo ведет себя (как можно ближе), как @foo().

Ответ 7

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

from functools import wraps

def default_arguments(*default_args, **default_kwargs):
  def _dwrapper(decorator):
    @wraps(decorator)
    def _fwrapper(*args, **kwargs):
      if callable(args[0]) and len(args) == 1 and not kwargs:
        return decorator(*default_args, **default_kwargs)(args[0])
      return decorator(*args, **kwargs)
    return _fwrapper
  return _dwrapper

Его можно использовать любым из способов.

from functools import lru_cache   # memoization decorator from Python 3

# apply decorator to decorator post definition
lru_cache = (default_arguments(maxsize=100)) (lru_cache)  
# could also be:
#   @default_arguments(maxsize=100)
#   class lru_cache(object):
#     def __init__(self, maxsize):
#       ...
#     def __call__(self, wrapped_function):
#       ...


@lru_cache   # this works
def fibonacci(n):
  ...

@lru_cache(200)   # this also works
def fibonacci(n):
  ...

Ответ 8

Если вы хотите эту функцию на нескольких декораторах, вы можете уклониться от шаблона кода с помощью декоратора для декоратора:

from functools import wraps
import inspect


def decorator_defaults(**defined_defaults):
    def decorator(f):
        args_names = inspect.getargspec(f)[0]

        def wrapper(*new_args, **new_kwargs):
            defaults = dict(defined_defaults, **new_kwargs)
            if len(new_args) == 0:
                return f(**defaults)
            elif len(new_args) == 1 and callable(new_args[0]):
                return f(**defaults)(new_args[0])
            else:
                too_many_args = False
                if len(new_args) > len(args_names):
                    too_many_args = True
                else:
                    for i in range(len(new_args)):
                        arg = new_args[i]
                        arg_name = args_names[i]
                        defaults[arg_name] = arg
                if len(defaults) > len(args_names):
                    too_many_args = True
                if not too_many_args:
                    final_defaults = []
                    for name in args_names:
                        final_defaults.append(defaults[name])
                    return f(*final_defaults)
                if too_many_args:
                    raise TypeError("{0}() takes {1} argument(s) "
                                    "but {2} were given".
                                    format(f.__name__,
                                           len(args_names),
                                           len(defaults)))
        return wrapper
    return decorator


@decorator_defaults(start_val="-=[", end_val="]=-")
def my_text_decorator(start_val, end_val):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            return "".join([f.__name__, ' ', start_val,
                            f(*args, **kwargs), end_val])
        return wrapper
    return decorator


@decorator_defaults(end_val="]=-")
def my_text_decorator2(start_val, end_val):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            return "".join([f.__name__, ' ', start_val,
                            f(*args, **kwargs), end_val])
        return wrapper
    return decorator


@my_text_decorator
def func1a(value):
    return value


@my_text_decorator()
def func2a(value):
    return value


@my_text_decorator2("-=[")
def func2b(value):
    return value


@my_text_decorator(end_val=" ...")
def func3a(value):
    return value


@my_text_decorator2("-=[", end_val=" ...")
def func3b(value):
    return value


@my_text_decorator("|> ", " <|")
def func4a(value):
    return value


@my_text_decorator2("|> ", " <|")
def func4b(value):
    return value


@my_text_decorator(end_val=" ...", start_val="|> ")
def func5a(value):
    return value


@my_text_decorator2("|> ", end_val=" ...")
def func5b(value):
    return value


print(func1a('My sample text'))  # func1a -=[My sample text]=-
print(func2a('My sample text'))  # func2a -=[My sample text]=-
print(func2b('My sample text'))  # func2b -=[My sample text]=-
print(func3a('My sample text'))  # func3a -=[My sample text ...
print(func3b('My sample text'))  # func3b -=[My sample text ...
print(func4a('My sample text'))  # func4a |> My sample text <|
print(func4b('My sample text'))  # func4b |> My sample text <|
print(func5a('My sample text'))  # func5a |> My sample text ...
print(func5b('My sample text'))  # func5b |> My sample text ...

Примечание: у него есть недостаток, когда вы не можете передать 1 аргумент как функцию декоратору.

Примечание2: если у вас есть советы/замечания о том, как улучшить этот декоратор, вы можете прокомментировать обзор кода: https://codereview.stackexchange.com/questions/78829/python-decorator-for-optional-arguments-decorator

Ответ 9

Здесь другая вариация, которая довольно кратка и не использует functools:

def decorator(*args, **kwargs):
    def inner_decorator(fn, foo=23, bar=42, abc=None):
        '''Always passed <fn>, the function to decorate.
        # Do whatever decorating is required.
        ...
    if len(args)==1 and len(kwargs)==0 and callable(args[0]):
        return inner_decorator(args[0])
    else:
        return lambda fn: inner_decorator(fn, *args, **kwargs)

В зависимости от того, можно ли вызывать inner_decorator только с одним параметром, можно сделать @decorator, @decorator(), @decorator(24) и т.д.

Это можно обобщить на "декоратор-декоратор":

def make_inner_decorator(inner_decorator):
    def decorator(*args, **kwargs):
        if len(args)==1 and len(kwargs)==0 and callable(args[0]):
            return inner_decorator(args[0])
        else:
            return lambda fn: inner_decorator(fn, *args, **kwargs)
    return decorator

@make_inner_decorator
def my_decorator(fn, a=34, b='foo'):
    ...

@my_decorator
def foo(): ...

@my_decorator()
def foo(): ...

@my_decorator(42)
def foo(): ...

Ответ 10

Вот другое решение, которое работает также, если необязательный аргумент является вызываемым:

def test_equal(func=None, optional_value=None):
    if func is not None and optional_value is not None:
        # prevent user to set func parameter manually
        raise ValueError("Don't set 'func' parameter manually")
    if optional_value is None:
        optional_value = 10  # The default value (if needed)

    def inner(function):
        def func_wrapper(*args, **kwargs):
            # do something
            return function(*args, **kwargs) == optional_value

        return func_wrapper

    if not func:
        return inner
    return inner(func)

Таким образом, оба синтаксиса будут работать:

@test_equal
def does_return_10():
    return 10

@test_equal(optional_value=20)
def does_return_20():
    return 20

# does_return_10() return True
# does_return_20() return True

Ответ 11

Здесь мое решение, написанное для python3. Он отличается от других тем, что определяет класс, вызываемый, а не функцию.

class flexible_decorator:

    def __init__(self, arg="This is default"):
        self.arg = arg

    def __call__(self, func):

        def wrapper(*args, **kwargs):
            print("Calling decorated function. arg '%s'" % self.arg)
            func(*args, **kwargs)

        return wrapper

Вам все равно нужно явно вызвать декоратор

@flexible_decorator()
def f(foo):
    print(foo)


@flexible_decorator(arg="This is not default")
def g(bar):
    print(bar)

Ответ 12

Я сделал простой пакет, чтобы решить проблему

Монтаж

Мастер pip install git+https://github.com/ferrine/biwrap ветки pip install git+https://github.com/ferrine/biwrap Последняя версия pip install biwrap

обзор

Некоторые оболочки могут иметь необязательные аргументы, и мы часто хотим избегать @wrapper() и использовать вместо них @wrapper.

Это работает для простой обертки

import biwrap

@biwrap.biwrap
def hiwrap(fn, hi=True):
    def new(*args, **kwargs):
        if hi:
            print('hi')
        else:
            print('bye')
        return fn(*args, **kwargs)
    return new

Определенная обертка может быть использована обоими способами

@hiwrap
def fn(n):
    print(n)
fn(1)
#> hi
#> 1

@hiwrap(hi=False)
def fn(n):
    print(n)
fn(1)
#> bye
#> 1

biwrap также работает для связанных методов

class O:
    @hiwrap(hi=False)
    def fn(self, n):
        print(n)

O().fn(1)
#> bye
#> 1

Методы/свойства класса тоже поддерживаются

class O:
    def __init__(self, n):
        self.n = n

    @classmethod
    @hiwrap
    def fn(cls, n):
        print(n)

    @property
    @hiwrap(hi=False)
    def num(self):
        return self.n


o = O(2)
o.fn(1)
#> hi
#> 1
print(o.num)
#> bye
#> 2

Функция как вызов тоже в порядке

def fn(n):
    print(n)

fn = hiwrap(fn, hi=False)
fn(1)
#> bye
#> 1

Ответ 13

Подобное решение, подобное проверке типа и длины аргументов с использованием вызываемых классов.

class decor(object):

def __init__(self, *args, **kwargs):
    self.decor_args = args
    self.decor_kwargs = kwargs

def __call__(self, *call_args, **call_kwargs):

    if callable(self.decor_args[0]) and len(self.decor_args) == 1:
        func = self.decor_args[0]
        return self.__non_param__call__(func, call_args, call_kwargs)
    else:
        func = call_args[0]
        return self.__param__call__(func)


def __non_param__call__(self, func, call_args, call_kwargs):
        print "No args"
        return func(*call_args, **call_kwargs)

def __param__call__(self, func):
    def wrapper(*args, **kwargs):
        print "With Args"
        return func(*args, **kwargs)
    return wrapper



@decor(a)
def test1(a):
    print 'test' + a

@decor
def test2(b):
    print 'test' + b

Ответ 14

Я был невероятно раздражен этой проблемой и в конце концов написал библиотеку для ее решения: decopatch.

Он поддерживает два стиля разработки: вложенный (как на фабриках декораторов Python) и плоский (на один уровень меньше вложенности). Вот как ваш пример будет реализован в плоском режиме:

from decopatch import function_decorator, DECORATED
from makefun import wraps

@function_decorator
def foo_register(method_name=None, method=DECORATED):
    if method_name is None:
        method.gw_method = method.__name__
    else:
        method.gw_method = method_name

    # create a signature-preserving wrapper
    @wraps(method)
    def wrapper(*args, **kwargs):
        method(*args, **kwargs)

    return wrapper

Обратите внимание, что я использую makefun.wraps вместо functools.wraps здесь так, что подпись полностью сохранена (оболочка не вызывается вообще, если аргументы являются недействительными).

decopatch поддерживает дополнительный стиль разработки, который я называю double-flat, который предназначен для создания оболочек с функцией сохранения подписи, подобных этой. Ваш пример будет реализован так:

from decopatch import function_decorator, WRAPPED, F_ARGS, F_KWARGS

@function_decorator
def foo_register(method_name=None,
                 method=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS):
    # this is directly the wrapper
    if method_name is None:
        method.gw_method = method.__name__
    else:
        method.gw_method = method_name

    method(*f_args, **f_kwargs)

Обратите внимание, что в этом стиле весь ваш код выполняется в вызовах method. Это может быть нежелательно - вы можете захотеть выполнять вещи только один раз во время декорации - для этого предыдущий стиль будет лучше.

Вы можете проверить, что оба стиля работают:

@foo_register
def my_function():
    print('hi...')

@foo_register('say_hi')
def my_function():
    print('hi...')

Пожалуйста, проверьте документацию для деталей.