web-dev-qa-db-fra.com

Faire des décorateurs avec des arguments optionnels

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

Exemple: Ce qui suit décore my_function avec foo_register au lieu de passer à decorator.

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

Exemple: Ce qui suit fonctionne comme prévu.

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

Si je veux que cela fonctionne correctement dans les deux applications (une avec method.__name__ et une en passant le nom), je dois vérifier dans foo_register pour voir si le premier argument est un décorateur, et si oui, je dois: return decorator(method_name) (à la place de return decorator). Ce genre de "vérification pour voir si c'est un callable" semble très hack. Existe-t-il un moyen plus agréable de créer un décorateur multi-usages comme celui-ci?

P.S. Je sais déjà que je peux exiger que le décorateur soit appelé, mais ce n'est pas une "solution". Je veux que l'API se sente naturelle. Ma femme adore décorer et je ne veux pas gâcher ça.

52
orokusaki

Glenn - Je devais le faire alors. Je suppose que je suis heureux qu'il n'y ait pas de moyen "magique" de le faire. Je les déteste.

Donc, voici ma propre réponse (noms de méthodes différents des précédents, mais concept identique):

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

Exemple d'utilisation (les deux versions fonctionnent de la même manière):

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

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

Le moyen le plus propre que je connaisse pour ce faire est le suivant:

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

Explication

Lorsque le décorateur est appelé sans arguments optionnels comme celui-ci:

@decorator
def function ...

La fonction est passée en tant que premier argument et decorate renvoie la fonction décorée, comme prévu.

Si le décorateur est appelé avec un ou plusieurs arguments optionnels comme celui-ci:

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

Ensuite, decorator est appelé avec l'argument de fonction avec la valeur None. Une fonction de décoration est renvoyée, comme prévu.

Python 3

Notez que la signature du décorateur ci-dessus peut être améliorée avec la syntaxe *, spécifique à Python 3 pour imposer l'utilisation sécurisée des arguments de mots clés. Remplacez simplement la signature de la fonction la plus externe par:

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

Grâce aux réponses fournies ici et ailleurs et à une série d’essais et d’erreurs, j’ai trouvé qu’il existe un moyen beaucoup plus simple et générique de faire prendre aux arguments des arguments optionnels. Il vérifie les arguments avec lesquels il a été appelé, mais il n'y a pas d'autre moyen de le faire.

La clé est de décorer votre décorateur.

Code générique de décorateur

Voici le décorateur décorateur (ce code est générique et peut être utilisé par toute personne ayant besoin d'un décorateur optionnel 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

Usage

Son utilisation est aussi simple que:

  1. Créez un décorateur comme d'habitude.
  2. Après le premier argument de la fonction cible, ajoutez vos arguments facultatifs.
  3. Décorer le décorateur avec optional_arg_decorator

Exemple:

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

Cas de test

Pour votre cas d'utilisation:

Donc, dans votre cas, pour enregistrer un attribut sur la fonction avec le nom de méthode transmis ou le __name__ si None:

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

Ajouter des méthodes décorées

Vous avez maintenant un décorateur utilisable avec ou sans arguments:

@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 :)'
35
Nicole

Que diriez-vous

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
9
Oscar

Code générique amélioré de décorateur

Voici mon adaptation de @ NickC's answer avec les améliorations suivantes:

  • les kwargs facultatifs peuvent être passés au décorateur décoré
  • le décorateur décoré peut être une méthode liée
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
7
Ryne Everett

Maintenant que ce vieux fil est de retour au sommet de toute façon, lemme suffit de rajouter une certaine décoratrice:

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

Maintenant, votre décorateur magique n'est plus qu'à une seule ligne!

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

En passant, cela fonctionne pour tout décorateur. Cela provoque simplement que @foo se comporte (aussi près que possible) comme @foo().

4
Niklas B.

Un décorateur générique pour les définitions de décorateur, exprimant que ce dernier accepte les arguments par défaut, qui sont définis si aucun n'est explicitement fourni.

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

Il peut être utilisé de deux manières.

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):
  ...
3
K3---rnc

Si vous voulez cette fonctionnalité sur plusieurs décorateurs, vous pouvez échapper au code standard avec un décorateur pour un décorateur:

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

Note: cela a l’inconvénient que vous ne pouvez pas passer 1 argument en tant que fonction au décorateur.

Note2: si vous avez des conseils/astuces pour améliorer ce décorateur, vous pouvez commenter en passant en revue le code: https://codereview.stackexchange.com/questions/78829/python-decorator-for-optional-arguments-decorator

1

Voici une autre solution qui fonctionne également si l'argument optionnel est appelable: 

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)

De cette façon, les deux syntaxes fonctionneront:

@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
MajorTom

J'ai été incroyablement ennuyé par ce problème et j'ai finalement écrit une bibliothèque pour le résoudre: decopatch .

Il prend en charge deux styles de développement: nested (comme dans les usines de décorateurs Python) et flat (un niveau d'imbrication de moins). Voici comment votre exemple serait implémenté en mode plat:

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

Notez que j'utilise makefun.wraps au lieu de functools.wraps ici pour que la signature soit entièrement préservée (le wrapper n'est pas appelé du tout si les arguments ne sont pas valides).

decopatch prend en charge un style de développement supplémentaire, que j'appelle double-flat , dédié à la création d'encapsuleurs de fonctions préservant la signature comme celui-ci. Votre exemple serait implémenté comme ceci:

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)

Notez que dans ce style, tout votre code est exécuté dans des appels à method. Cela peut ne pas être souhaitable - vous voudrez peut-être effectuer des choses une fois au moment de la décoration seulement - pour cela, le style précédent serait meilleur.

Vous pouvez vérifier que les deux styles fonctionnent:

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

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

Veuillez vérifier la documentation pour plus de détails.

0
smarie

Voici une autre variante assez concise qui n’utilise pas 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)

Selon que inner_decorator peut être appelé avec un seul paramètre, on peut alors faire @decorator, @decorator(), @decorator(24) etc.

Ceci peut être généralisé à un 'décorateur décorateur':

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(): ...
0
Julian Smith

Une solution similaire à celle vérifiant le type et la longueur des arguments à l'aide de classes appelables

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
0
Gary

J'ai fait un paquet simple pour résoudre le problème

Installation

Branche principale pip install git+https://github.com/ferrine/biwrap Dernière version pip install biwrap

Vue d'ensemble

Certains wrappers peuvent avoir des arguments optionnels et nous voulons souvent éviter les appels @wrapper() et utiliser plutôt @wrapper.

Cela fonctionne pour un simple emballage

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

Le wrapper défini peut être utilisé dans les deux sens

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

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

biwrap fonctionne également pour les méthodes liées

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

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

Les méthodes/propriétés de classe sont également supportées

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

Fonctionne comme si l'appel est bien aussi

def fn(n):
    print(n)

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

Voici ma solution, écrite pour python3. Son approche est différente des autres car elle définit une classe appelable plutôt qu'une fonction.

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

Vous devez toujours appeler explicitement le décorateur

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


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