74 votes

Créer des décorateurs avec des arguments optionnels

from functools import wraps

def foo_register(method_name=None):
    """Effectue des trucs."""
    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 : Le code suivant décore my_function avec foo_register au lieu d'atteindre directement decorator.

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

Exemple : Le code suivant fonctionne comme prévu.

@foo_register('dire_bonjour')
def my_function():
    print('salut...')

Si je veux que cela fonctionne correctement dans les deux applications (en utilisant method.__name__ et en passant le nom), je dois vérifier à l'intérieur de foo_register si le premier argument est un décorateur, et si c'est le cas, je dois : return decorator(method_name) (au lieu de return decorator). Ce genre de "vérification si c'est callable" semble très bricolé. Y a-t-il un moyen plus élégant de créer un décorateur multi-usage comme celui-ci ?

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

2 votes

Voici la réponse. La fonction doit soit être une fonction de décorateur, soit une fonction qui renvoie une fonction de décorateur, et non pas magiquement l'une ou l'autre en fonction de ses arguments.

7 votes

@Glenn mais sa femme aime décorer. Et c'est un défi intéressant.

3 votes

Il ne me restait plus qu'à ajouter deux lignes de code supplémentaires (dans la réponse ci-dessous), ce qui équivaut à environ 180 octets d'économie. Cela signifie que je n'ai pas besoin d'acheter un nouveau disque dur, donc ma femme peut continuer à décorer.

77voto

Patbenavente Points 43

La manière la plus propre que je connaisse pour faire cela est la suivante:

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 ceci:

@decorator
def function ...

La fonction est passée en premier argument et le décorateur retourne la fonction décorée, comme prévu.

Si le décorateur est appelé avec un ou plusieurs arguments optionnels comme ceci:

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

Alors le décorateur est appelé avec l'argument de fonction avec la valeur None, donc une fonction qui décore est retourné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 renforcer 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, ...):

7 votes

Si l'on accepte l'exigence d'arguments de mot-clé au lieu d'arguments positionnels (ce que je fais), c'est de loin la meilleure réponse.

1 votes

Je suis d'accord, cela devrait être la réponse. C'est bien et clair.

4 votes

Vous pouvez imposer la nécessité des arguments de mots-clés en ajoutant * à la définition de votre fonction. Par exemple, def decorator(original_function=None, *, argument1=None, argument2=None, ...):

44voto

NickC Points 13729

À l'aide des réponses ici et ailleurs et avec beaucoup d'essais et d'erreurs, j'ai trouvé qu'il existe en réalité un moyen beaucoup plus facile et générique de rendre les décorateurs facultatifs. 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 de décorateur générique

Voici le décorateur de décorateur (ce code est générique et peut être utilisé par quiconque a besoin d'un décorateur avec des arguments facultatifs):

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

Utilisation

L'utiliser 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écorez le décorateur avec optional_arg_decorator

Exemple:

@optional_arg_decorator
def example_decorator_with_args(fn, optional_arg = 'Valeur par défaut'):
    ...
    return fn

Cas de test

Pour votre cas d'utilisation:

Ainsi, pour votre cas, pour enregistrer un attribut sur la fonction avec le nom de méthode passé 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

Maintenant vous avez un décorateur qui est utilisable avec ou sans arguments:

@register_method('Nom personnalisé')
def custom_name():
    pass

@register_method
def default_name():
    pass

assert custom_name.gw_method == 'Nom personnalisé'
assert default_name.gw_method == 'default_name'

print 'Test réussi :)'

0 votes

Ah oui, je pensais que cela fonctionnerait, j'étais sur le point d'écrire cela moi-même.. +1

4 votes

Dans le cas où votre décorateur reçoit une classe en tant que paramètre, cette solution NE fonctionnera PAS car callable(args[0]) renvoie True dans les deux cas. Lorsque vous décorez la fonction ET lorsque vous l'appelez.

28voto

orokusaki Points 10993

Glenn - Je devais le faire alors. Je suppose que je suis content qu'il n'y ait pas de façon "magique" de le faire. Je déteste ça.

Alors, voici ma propre réponse (les noms des méthodes sont différents de ci-dessus, mais le même concept):

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

@register_gw_method('dire_bonjour')
def my_function():
    print('salut...')

1 votes

C'est une fonction avec un comportement radicalement différent en fonction de ses arguments. C'est ce que je veux dire par magique: elle "découvre ce que vous voulez dire" au lieu de s'attendre à ce que l'utilisateur dise ce qu'il veut dire pour commencer.

1 votes

À mon avis, le concept même des décorateurs est assez magique. Pas magique comme les Lucky Charms, mais magique tout de même. Je pense que pour rendre la femme vraiment heureuse, il devrait y avoir un décorateur décorateur dans cette situation qui utilise des arguments par défaut si aucun n'est invoqué. Bien sûr, cela ne fonctionnerait pas s'il est effectivement passé un appelable.

2 votes

@intuited - Quand je code un décorateur, j'ai l'impression de regarder la scène du van/pont du film "Inception". Je suis d'accord qu'ils ne sont pas très amusants à maintenir, mais ils aident vraiment à rendre une bibliothèque plus conviviale (c'est-à-dire, un détail d'implémentation laid).

15voto

Oscar Points 41

Que diriez-vous de

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

15 votes

Il n'y a pas de telles choses que des fils de discussion morts sur stackoverflow. Si la meilleure réponse arrive trop tard pour l'auteur original : tant pis. Mais pour d'autres qui découvrent cela plus tard à travers une recherche, il est toujours précieux de répondre si votre réponse est précieuse.

4voto

Niklas B. Points 40619

Maintenant que ce vieux fil est remonté de toute façon, laissez-moi juste ajouter un peu de Décorateur-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

Maintenant votre décorateur magique est juste à une seule ligne de distance !

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

En passant, cela fonctionne pour n'importe quel décorateur. Cela fait simplement en sorte que @foo se comporte (aussi près que possible) de @foo().

0 votes

Attention: type est appelable. Considérez ajouter and not (type(args[0]) == type and issubclass(args[0], Exception)) à la condition dans le cas où votre décorateur prend des Exceptions en tant qu'arguments (comme le fait celui-ci).

0 votes

@Simon: N'hésite pas à modifier en conséquence :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X