Создание декораторов с необязательными аргументами - 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 ]

0 голосов
/ 13 января 2018

Я сделал простой пакет для решения проблемы

Установка

Мастер ветвь 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
0 голосов
/ 13 октября 2017

Вот мое решение, написанное для 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)
0 голосов
/ 14 декабря 2016

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

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
0 голосов
/ 12 января 2016

Вот еще один вариант, который довольно лаконичен и не использует 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(): ...
...