Préserver les signatures des fonctions décorées

111

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 la journalisation, implémenter la 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 y a toujours un problème: la signature de la fonction est erronée. L'information "* 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

C'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 et d'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. Veuillez ignorer ceci; funny_function est juste un exemple aléatoire. ]

2 - Ne pas utiliser de décorateur, ni utiliser de 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 bien pour un ensemble de fonctions qui ont une signature identique, mais c'est inutile en général. Comme je l'ai dit au début, je veux pouvoir utiliser les décorateurs de manière entièrement générique.

Je recherche une solution tout à fait générale et automatique.

La question est donc la suivante: existe-t-il un moyen d'éditer 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 extraire ces informations? Comment dois-je construire la fonction décorée - avec exec?

D'autres approches?

Fredrik Johansson
la source
1
Jamais dit "dépassé". Je me demandais plus ou moins ce qui inspect.Signatureajoutait à la gestion des fonctions décorées.
NightShadeQueen

Réponses:

79
  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()from stdlib préserve 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 il n'y conserve pas la signature:

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

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

jfs
la source
La vôtre n'était pas la première réponse, mais la plus complète à ce jour :-) Je préférerais en fait une solution n'impliquant pas de module tiers, mais en regardant la source du module décorateur, c'est assez simple pour que je puisse copiez-le simplement.
Fredrik Johansson le
1
@MarkLodato: functools.wraps()préserve déjà les signatures dans Python 3.4+ (comme indiqué dans la réponse). Voulez-vous dire que la configuration wrapper.__signature__aide sur les versions antérieures? (quelles versions avez-vous testées?)
jfs
1
@MarkLodato: help()affiche la signature correcte sur Python 3.4. Pourquoi pensez-vous qu'il functools.wraps()est cassé et non IPython?
jfs
1
@MarkLodato: il est cassé si nous devons écrire du code pour le réparer. Étant donné que cela help()produit le résultat correct, la question est de savoir quel logiciel doit être corrigé: functools.wraps()ou IPython? Dans tous les cas, l'attribution manuelle __signature__est au mieux une solution de contournement - ce n'est pas une solution à long terme.
jfs
1
On dirait que inspect.getfullargspec()ne renvoie toujours pas la signature correcte pour functools.wrapspython 3.4 et que vous devez utiliser à la inspect.signature()place.
Tuukka Mustonen
16

Ceci est résolu avec la bibliothèque standard de Python functoolset en particulier la functools.wrapsfonction, qui est conçue pour « mettre à jour une fonction wrapper pour qu'elle ressemble à la fonction enveloppée ». 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é dans Python 3, cela 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. Lorsqu'il est 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
Timur
la source
Je ne sais pas si c'est Sphinx, mais cela ne semble pas fonctionner lorsque la fonction encapsulée est une méthode d'une classe. Sphinx continue de signaler la signature d'appel du décorateur.
alphabetasoup
9

Il existe un moduledecorator décorateur avec 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: JF Sebastian a souligné que je n'ai pas modifié la args_as_intsfonction - elle est corrigée maintenant.

DzinX
la source
8

Jetez un œil au module décorateur - en particulier le décorateur décorateur, qui résout ce problème.

Brian
la source
6

Deuxième option:

  1. Installez le module wrapt:

$ easy_install wrapt

wrapt ont un bonus, préservez 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
macm
la source
2

Comme commenté ci-dessus dans la réponse de jfs ; si vous êtes préoccupé par la signature en termes d'apparence ( help, et inspect.signature), utilisezfunctools.wraps est parfaitement bien.

Si vous êtes préoccupé par la signature en termes de comportement (en particulier TypeErroren cas de discordance d'arguments), functools.wrapsne la préserve pas. Vous devriez plutôt utiliser decoratorpour cela, ou ma généralisation de son moteur de base, 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 cet article surfunctools.wraps .

smarie
la source
1
De plus, le résultat de inspect.getfullargspecn'est pas conservé en appelant functools.wraps.
laike9m
Merci pour le commentaire supplémentaire utile @ laike9m!
smarie