Question Que fait functools.wraps?


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


416
2017-11-21 14:53


origine


Réponses:


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

print f.__name__

il imprimera with_logging parce que c'est le nom de votre nouvelle fonction. En fait, si vous regardez la documentation pour f, ce sera vide parce que with_logging n'a pas de docstring, et le docstring que vous avez écrit ne sera plus là. En outre, si vous regardez le résultat de pydoc pour cette fonction, il ne sera pas répertorié comme prenant un argument x; à la place, il sera répertorié comme prenant *args et **kwargs parce que c'est ce que with_logging prend.

Si utiliser un décorateur signifiait toujours perdre cette information sur une fonction, cela poserait un grave problème. Voilà 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, docstring, la liste des arguments, etc. Et depuis wraps 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'

744
2017-11-21 14:53



J'utilise très souvent des cours plutôt que des fonctions pour mes décorateurs. J'avais des problèmes avec cela, car un objet n'aura pas 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 ce qui était assez difficile à tracer là où Django signalait l'erreur "l'objet n'a pas d'attribut"__name__"Malheureusement, pour les décorateurs de classe, je ne crois pas que @wrap fera le travail. J'ai plutôt créé un cours de décorateur de base comme celui-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 transmet 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 ceci:

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

        return self.func(*args)

20
2017-12-03 23:46



  1. Prérequis: Vous devez savoir utiliser des décorateurs et spécialement avec des wraps. Ce commentaire explique un peu clair ou cela lien explique aussi plutôt bien.

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

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

Donc, le décorateur @wraps appelle réellement functools.partial (func [, * args] [, ** keywords]).

La définition functools.partial () dit que

Le partial () est utilisé pour une application de fonction partielle qui "fige" une partie des arguments d'une fonction et / ou des mots-clés 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 est par défaut 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 passe votre fonction wrapper en paramètre. Le partial () à la fin renvoie la version simplifiée, c'est-à-dire l'objet de ce qui est dans la fonction wrapper et non la fonction wrapper elle-même.


1
2018-03-10 03:14



En bref, functools.wraps est juste une fonction régulière. Considérons cet exemple officiel. Avec l'aide du code source, nous pouvons voir plus de détails sur la mise en œuvre et les étapes en cours comme suit:

  1. enveloppes (f) renvoie un objet, disons O1. C'est un objet de la classe Partielle
  2. La prochaine étape est @ O1 ... qui est la notation de décorateur en python. Ça veut dire

wrapper = O1 .__ appel __ (wrapper)

Vérification de la mise en œuvre de __appel__, on voit qu'après cette étape, (le côté gauche)wrapper devient l'objet résultant de self.func (* self.args, * args, ** newkeywords) Vérification de la création de O1 dans __Nouveau__, nous savons self.func est la fonction update_wrapper. Il utilise le paramètre * args, le côté droit wrapper, comme son 1er paramètre. Vérification de la dernière étape de update_wrapper, on peut voir le côté droit wrapper est renvoyé, avec certains attributs modifiés selon les besoins.


0
2018-03-30 04:27