Question

Supposons que j'ai écrit un décorateur qui fait quelque chose de très générique. Par exemple, il peut convertir tous les arguments en un type spécifique, effectuer une journalisation, implémenter une mémorisation, etc.

Voici un exemple:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

Tout va bien jusqu'à présent. Il y a cependant un problème. La fonction décorée ne conserve pas la documentation de la fonction d'origine:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

Heureusement, il existe une solution de contournement:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

Cette fois, le nom de la fonction et la documentation sont corrects:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

Mais il reste un problème: la signature de la fonction est fausse. Les informations & *; args, ** kwargs " est presque inutile.

Que faire? Je peux penser à deux solutions de contournement simples mais imparfaites:

1 - Incluez la signature correcte dans la docstring:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

Ceci est mauvais à cause de la duplication. La signature ne sera toujours pas affichée correctement dans la documentation générée automatiquement. Il est facile de mettre à jour la fonction sans oublier de changer la docstring ou de faire une faute de frappe. [ Et oui, je suis conscient du fait que la docstring duplique déjà le corps de la fonction. S'il vous plaît ignorer ceci; funny_function est juste un exemple aléatoire. ]

2 - N'utilisez pas de décorateur, ni un décorateur spécial pour chaque signature spécifique:

def funny_functions_decorator(f):
    def g(x, y, z=3):
        return f(int(x), int(y), z=int(z))
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

Cela fonctionne très bien pour un ensemble de fonctions ayant une signature identique, mais inutile en général. Comme je l'ai dit au début, je veux pouvoir utiliser les décorateurs de manière générique.

Je recherche une solution entièrement générale et automatique.

La question est donc la suivante: existe-t-il un moyen de modifier la signature de la fonction décorée après sa création?

Sinon, puis-je écrire un décorateur qui extrait la signature de la fonction et utilise cette information au lieu de "* kwargs, ** kwargs"? lors de la construction de la fonction décorée? Comment puis-je extraire cette information? Comment dois-je construire la fonction décorée - avec exec?

D'autres approches?

Était-ce utile?

La solution

  1. Installez le module décorateur :

    $ pip install decorator
    
  2. Adapter la définition de args_as_ints () :

    import decorator
    
    @decorator.decorator
    def args_as_ints(f, *args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    
    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x*y + 2*z
    
    print funny_function("3", 4.0, z="5")
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    # 
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z
    

Python 3.4 +

functools.wraps () à partir de stdlib conserve les signatures depuis Python 3.4:

import functools


def args_as_ints(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

functools.wraps () est disponible au moins depuis Python 2.5 mais la signature n’y est pas conservée:

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
#    Computes x*y + 2*z

Remarque: * args, ** kwargs au lieu de x, y, z = 3 .

Autres conseils

Ceci est résolu avec la bibliothèque standard de Python functools et plus précisément functools.wraps , qui est conçue pour" mettre à jour une fonction wrapper afin qu'elle ressemble à la fonction "". Son comportement dépend de la version de Python, cependant, comme indiqué ci-dessous. Appliqué à l'exemple de la question, le code ressemblerait à ceci:

from functools import wraps

def args_as_ints(f):
    @wraps(f) 
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

Une fois exécuté en Python 3, ceci produirait ce qui suit:

>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

Son seul inconvénient est que dans Python 2, cependant, il ne met pas à jour la liste des arguments de la fonction. Une fois exécuté en Python 2, il produira:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

Il existe un module décorateur avec décorateur décorateur que vous pouvez utiliser:

@decorator
def args_as_ints(f, *args, **kwargs):
    args = [int(x) for x in args]
    kwargs = dict((k, int(v)) for k, v in kwargs.items())
    return f(*args, **kwargs)

Ensuite, la signature et l'aide de la méthode sont conservées:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

EDIT: J. F. Sebastian a fait remarquer que je n’avais pas modifié la fonction args_as_ints - elle est maintenant corrigée.

Consultez le module decorator , plus précisément le < un href = "http://www.phyast.pitt.edu/~micheles/python/documentation.html#decorator-is-a-decorator" rel = "noreferrer"> décorateur décorateur, qui résout ce problème .

Deuxième option:

  1. Installer le module wrapt:

$ easy_install wrapt

wrapp a un bonus, conserve la signature de la classe.


import wrapt
import inspect

@wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, z=5)) # 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*z

Comme indiqué ci-dessus dans la réponse de jfs ; si vous êtes préoccupé par la signature en termes d’apparence ( help et inspect.signature ), utiliser functools.wraps convient parfaitement.

Si vous êtes préoccupé par la signature en termes de comportement (en particulier TypeError en cas de non concordance des arguments), functools.wraps ne la préserve pas. Vous devriez plutôt utiliser decorator pour cela, ou ma généralisation de son moteur principal, nommé makefun .

from makefun import wraps

def args_as_ints(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper executes")
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# wrapper executes
# 22

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

funny_function(0)  
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)

Voir aussi ce message sur functools.wraps .

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top