Question

Dans un commentaire sur cette Pour répondre à une autre question , quelqu'un a déclaré ne pas savoir exactement ce que faisait functools.wraps . Donc, je pose cette question pour qu’il y ait un enregistrement de cela sur StackOverflow pour référence future: que fait functools.wraps , exactement?

Était-ce utile?

La solution

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 vous dites

@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 f est remplacée par la fonction with_logging. Malheureusement, cela signifie que si vous dites ensuite

print(f.__name__)

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

Si utiliser un décorateur signifiait toujours perdre cette information sur une fonction, ce serait un problème grave. C'est pourquoi nous avons 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 wraps est lui-même un décorateur, le code suivant agit correctement:

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'

Autres conseils

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 attributs attendus d'une fonction. Par exemple, un objet n'aura pas l'attribut __ nom __ . J'ai eu un problème spécifique avec cela qui était assez difficile à localiser et où Django a signalé l'erreur "l'objet n'a pas d'attribut" __ nom __ ". Malheureusement, pour les décorateurs de style classique, je ne pense pas que @wrap fera l'affaire. J'ai plutôt créé une classe de décorateur de base comme celle-ci:

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 un proxy à tous les appels d'attributs à la fonction en cours de décoration. Donc, 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)

À partir de 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 le __ module __ , __ nom __ , __ nom qualifié __ , __ doc __ et __ annotations __ attributs de f sur g . Cette liste par défaut est dans WRAPPER_ASSIGNMENTS , vous pouvez la voir dans source de functools .
  • il met à jour le __ dict __ de g avec tous les éléments de f .__ dict __ . (voir WRAPPER_UPDATES dans le source)
  • définit un nouvel attribut __ wrapping __ = f sur g

La conséquence est que g apparaît avec les mêmes 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 vraiment vrai: c’est simplement que inspect.signature suit les chaînes d’encapsuleur par défaut. Vous pouvez le vérifier en utilisant inspect.signature (g, follow_wrapped = False) comme expliqué dans le document doc . Cela a des conséquences fâcheuses:

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

Il y a maintenant un peu de confusion entre functools.wraps et les décorateurs, car un cas d'utilisation très fréquent pour le développement de décorateurs consiste à envelopper des fonctions. Mais les deux sont des concepts complètement indépendants. Si vous souhaitez comprendre la différence, j'ai implémenté des bibliothèques d'assistance pour les deux: decopatch à écrire les décorateurs et makefun pour remplacer le @wraps . Notez que makefun repose sur le même truc éprouvé que la célèbre bibliothèque decorator .

c'est le code source sur les 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)
  1. Prérequis: Vous devez savoir utiliser les décorateurs et spécialement les wraps. Ce commentaire explique un peu mieux ou ce link explique aussi cela plutôt bien.

  2. Chaque fois que nous utilisons For, par exemple: @wraps suivi de notre propre fonction wrapper. Selon les détails donnés dans ce lien , il est indiqué 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.

     

Cela équivaut à partiel (update_wrapper, wrapped = wrapped, assigné = assigné, mis à jour = mis à jour).

Le décorateur @wraps appelle donc functools.partial (func [, * args] [, ** mots-clés]).

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

  

La valeur partial () est utilisée pour les applications de fonction partielle qui gèlent & # 8220; & # 8221; une partie des arguments et / ou des mots-clés d’une fonction entraînant la création d’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 est par défaut égal à 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 appelle l'appel à partial () et lui transmet votre fonction d'emballage en tant que paramètre. Le partial () à la fin retourne 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.

En bref, functools.wraps n'est qu'une fonction normale. Examinons cet exemple officiel . Avec l'aide de code source , nous pouvons voyez 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 >
  2. L'étape suivante est @ O1 ... , qui est la notation du décorateur en python. Cela signifie
  

wrapper = O1 .__ appel __ (wrapper)

Vérification de la mise en œuvre de add > , nous voyons qu'après cette étape, wrapper (à gauche) devient l'objet résultant de self.func (* self.args, * args, ** newkeywords ) Vérification de la création de O1 dans __ new __ , nous savons que self.func est la fonction update_wrapper . . Il utilise le paramètre * args , le wrapper situé à droite, comme premier paramètre. En vérifiant la dernière étape de update_wrapper , vous pouvez voir que le wrapper situé à droite est renvoyé, avec certains des attributs modifiés en fonction des besoins.

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