Que fait functools.wraps?

651

Dans un commentaire sur cette réponse à une autre question , quelqu'un a dit qu'il n'était pas sûr de ce qu'il functools.wrapsfaisait. Donc, je pose cette question afin qu'il y en ait un enregistrement sur StackOverflow pour référence future: que fait functools.wraps-il exactement?

Eli Courtwright
la source

Réponses:

1070

Lorsque vous utilisez un décorateur, vous remplacez une fonction par une autre. En d'autres termes, si vous avez un décorateur

def logged(func):
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

alors quand tu dis

@logged
def f(x):
   """does some math"""
   return x + x * x

c'est exactement la même chose que de dire

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)

et votre fonction fest remplacée par la fonction with_logging. Malheureusement, cela signifie que si vous dites

print(f.__name__)

il s'imprimera with_loggingcar c'est le nom de votre nouvelle fonction. En fait, si vous regardez la docstring f, elle sera vide car with_loggingn'a pas de docstring, et donc la docstring que vous avez écrite ne sera plus là. De plus, si vous regardez le résultat pydoc pour cette fonction, il ne sera pas répertorié comme prenant un seul argument x; au lieu de cela, il sera répertorié comme prenant *argset **kwargsparce que c'est ce que prend with_logging.

Si utiliser un décorateur signifiait toujours perdre ces informations sur une fonction, ce serait un problème grave. C'est pourquoi nous l'avons fait functools.wraps. Cela prend une fonction utilisée dans un décorateur et ajoute la fonctionnalité de copie sur le nom de la fonction, la docstring, la liste des arguments, etc. Et comme wrapsc'est lui-même un décorateur, le code suivant fait la bonne chose:

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

print(f.__name__)  # prints 'f'
print(f.__doc__)   # prints 'does some math'
Eli Courtwright
la source
7
Oui, je préfère éviter le module décorateur car functools.wraps fait partie de la bibliothèque standard et n'introduit donc pas une autre dépendance externe. Mais le module décorateur résout en effet le problème d'aide, qui, espérons-le, fonctionnera un jour également avec functools.wraps.
Eli Courtwright
6
voici un exemple de ce qui peut arriver si vous n'utilisez pas de wraps: les tests doctools peuvent soudainement disparaître. c'est parce que doctools ne peut pas trouver les tests dans les fonctions décorées à moins que quelque chose comme wraps () ne les ait copiés.
Andrew Cooke du
88
pourquoi avons-nous besoin functools.wrapspour ce travail, ne devrait-il pas simplement faire partie du motif de décoration en premier lieu? quand ne voudriez-vous pas utiliser @wraps?
wim
56
@wim: J'ai écrit quelques décorateurs qui font leur propre version @wrapsafin d'effectuer différents types de modifications ou d'annotations sur les valeurs copiées. Fondamentalement, c'est une extension de la philosophie Python qu'explicite vaut mieux qu'implicite et les cas spéciaux ne sont pas assez spéciaux pour enfreindre les règles. (Le code est beaucoup plus simple et le langage plus facile à comprendre s'il @wrapsdoit être fourni manuellement, plutôt que d'utiliser une sorte de mécanisme spécial de désinscription.)
ssokolow
35
@LucasMalor Tous les décorateurs n'encapsulent pas les fonctions qu'ils décorent. Certains appliquent des effets secondaires, comme les enregistrer dans une sorte de système de recherche.
ssokolow
22

J'utilise très souvent des classes, plutôt que des fonctions, pour mes décorateurs. J'avais quelques problèmes avec cela car un objet n'aura pas tous les mêmes attributs que ceux attendus d'une fonction. Par exemple, un objet n'aura pas l'attribut __name__. J'ai eu un problème spécifique avec cela qui était assez difficile à trouver où Django rapportait l'erreur "l'objet n'a pas d'attribut" __name__"". Malheureusement, pour les décorateurs de classe, je ne pense pas que @wrap fera l'affaire. J'ai plutôt créé une classe de décorateur de base comme ceci:

class DecBase(object):
    func = None

    def __init__(self, func):
        self.__func = func

    def __getattribute__(self, name):
        if name == "func":
            return super(DecBase, self).__getattribute__(name)

        return self.func.__getattribute__(name)

    def __setattr__(self, name, value):
        if name == "func":
            return super(DecBase, self).__setattr__(name, value)

        return self.func.__setattr__(name, value)

Cette classe envoie par proxy tous les appels d'attribut à la fonction en cours de décoration. Ainsi, vous pouvez maintenant créer un décorateur simple qui vérifie que 2 arguments sont spécifiés comme suit:

class process_login(DecBase):
    def __call__(self, *args):
        if len(args) != 2:
            raise Exception("You can only specify two arguments")

        return self.func(*args)
Josh
la source
7
Comme le @wrapsdit la documentation de , @wrapsc'est juste une fonction pratique pour functools.update_wrapper(). En cas de décorateur de classe, vous pouvez appeler update_wrapper()directement depuis votre __init__()méthode. Donc, vous n'avez pas besoin de créer DecBasedu tout, vous pouvez simplement inclure sur __init__()de process_loginla ligne: update_wrapper(self, func). C'est tout.
Fabiano
15

Depuis python 3.5+:

@functools.wraps(f)
def g():
    pass

Est un alias pour g = functools.update_wrapper(g, f). Il fait exactement trois choses:

  • il copie les __module__, __name__, __qualname__, __doc__et __annotations__attributs de fsur g. Cette liste par défaut est WRAPPER_ASSIGNMENTSdedans, vous pouvez la voir dans la source functools .
  • il met à jour la __dict__de gtous les éléments de f.__dict__. (voir WRAPPER_UPDATESdans la source)
  • il définit un nouvel __wrapped__=fattribut surg

La conséquence est qu'il gapparaît comme ayant le même nom, docstring, nom de module et signature que f. Le seul problème est qu'en ce qui concerne la signature, ce n'est pas vrai: c'est juste que inspect.signaturesuit les chaînes de wrapper par défaut. Vous pouvez le vérifier en utilisant inspect.signature(g, follow_wrapped=False)comme expliqué dans la doc . Cela a des conséquences gênantes:

  • le code wrapper s'exécutera même lorsque les arguments fournis ne sont pas valides.
  • le code wrapper ne peut pas accéder facilement à un argument en utilisant son nom, à partir des * args, ** kwargs reçus. En effet, il faudrait gérer tous les cas (positionnel, mot-clé, par défaut) et donc utiliser quelque chose comme Signature.bind().

Maintenant, il y a un peu de confusion entre functools.wrapset les décorateurs, car un cas d'utilisation très fréquent pour développer des décorateurs est d'encapsuler des fonctions. Mais les deux sont des concepts complètement indépendants. Si vous êtes intéressé à comprendre la différence, j'ai implémenté des bibliothèques d'aide pour les deux: decopatch pour écrire facilement les décorateurs et makefun pour fournir un remplacement préservant la signature @wraps. Notez que makefunrepose sur la même astuce éprouvée que la célèbre decoratorbibliothèque.

smarie
la source
3

voici le code source des wraps:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')

WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):

    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

   Returns a decorator that invokes update_wrapper() with the decorated
   function as the wrapper argument and the arguments to wraps() as the
   remaining arguments. Default arguments are as for update_wrapper().
   This is a convenience function to simplify applying partial() to
   update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)
Baliang
la source
2
  1. Prérequis: Vous devez savoir utiliser des décorateurs et spécialement avec des wraps. Ce commentaire l' explique un peu clair ou ce lien l' explique aussi assez bien.

  2. Chaque fois que nous utilisons Pour par exemple: @wraps suivi de notre propre fonction wrapper. Selon les détails donnés dans ce lien , il est dit que

functools.wraps est une fonction pratique pour appeler update_wrapper () en tant que décorateur de fonction, lors de la définition d'une fonction wrapper.

C'est équivalent à partial (update_wrapper, wrapped = wrapped, assigné = assigné, mis à jour = mis à jour).

Donc, @wraps decorator appelle en fait functools.partial (func [, * args] [, ** mots-clés]).

La définition de functools.partial () dit que

Le partial () est utilisé pour une application de fonction partielle qui «fige» une partie des arguments et / ou des mots-clés d'une fonction résultant en un nouvel objet avec une signature simplifiée. Par exemple, partial () peut être utilisé pour créer un appelable qui se comporte comme la fonction int () où l'argument de base par défaut est deux:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

Ce qui m'amène à la conclusion que @wraps donne un appel à partial () et lui transmet votre fonction wrapper comme paramètre. Le partial () à la fin renvoie la version simplifiée, c'est-à-dire l'objet de ce qui est à l'intérieur de la fonction wrapper et non la fonction wrapper elle-même.

3rdi
la source
-4

En bref, functools.wraps n'est qu'une fonction régulière. Prenons cet exemple officiel . Avec l'aide du code source , nous pouvons voir plus de détails sur l'implémentation et les étapes en cours comme suit:

  1. wraps (f) renvoie un objet, par exemple O1 . C'est un objet de la classe Partial
  2. L'étape suivante est @ O1 ... qui est la notation décoratrice en python. Ça veut dire

wrapper = O1 .__ appel __ (wrapper)

En vérifiant l'implémentation de __call__ , nous voyons qu'après cette étape, (le côté gauche) le wrapper devient l'objet résultant par self.func (* self.args, * args, ** newkeywords) Vérification de la création de O1 dans __new__ , nous connaître self.func est la fonction update_wrapper . Il utilise le paramètre * args , le wrapper de droite , comme son premier paramètre. En vérifiant la dernière étape de update_wrapper , on peut voir que le wrapper de droite est retourné, avec certains attributs modifiés selon les besoins.

Yong Yang
la source