Создание декораторов с необязательными аргументами - PullRequest
56 голосов
/ 08 октября 2010
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 чувствовал себя естественно. Моя жена любит украшать, и я не хочу разрушать это.

Ответы [ 14 ]

50 голосов
/ 07 июля 2014

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

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, ...):
37 голосов
/ 07 января 2014

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

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

Общий код декоратора-декоратора

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

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

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

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

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

Пример:

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

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

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

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

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

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

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

@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 :)'
27 голосов
/ 08 октября 2010

Гленн - я должен был сделать это тогда.Думаю, я рад, что нет «волшебного» способа сделать это.Я ненавижу их.

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

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...')
10 голосов
/ 24 апреля 2012

Как насчет

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
7 голосов
/ 30 августа 2015

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

Вот мой вариант ответа @ Николь со следующими улучшениями:

  • необязательные 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
4 голосов
/ 24 апреля 2012

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

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().

3 голосов
/ 17 мая 2013

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

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):
  ...
1 голос
/ 11 марта 2019

Я был невероятно раздражен этой проблемой и в итоге написал библиотеку для ее решения: 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...')

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

1 голос
/ 28 января 2015

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

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

0 голосов
/ 07 июня 2018

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

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
...