Dans un commentaire sur ceci réponse à une autre question , quelqu'un a dit qu'il n'était pas sûr de ce que functools.wraps
faisait. Donc, je pose cette question pour qu'il y ait un enregistrement de cela sur StackOverflow pour référence future: que fait exactement functools.wraps
?
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.__+ " 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ée par la fonction with_logging. Malheureusement, cela signifie que si vous dites ensuite
print(f.__name__)
cela imprimera with_logging
parce que c'est le nom de votre nouvelle fonction. En fait, si vous regardez la docstring pour f
, elle sera vide car with_logging
ne possède pas de docstring et le docstring que vous avez écrit ne sera plus là. De plus, si vous regardez le résultat de 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 *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, le problème serait sérieux. 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 puisque wraps
est lui-même un décorateur, le code suivant fait le bon choix:
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__+ " 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'
J'utilise très souvent des cours 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 __name__
. J'ai eu un problème spécifique avec ce qui était assez difficile à retracer où Django signalait l'erreur "l'objet n'a pas d'attribut '__name__
'". 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 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 proxie 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:
__module__
, __name__
, __qualname__
, __doc__
et __annotations__
de f
sur g
. Cette liste par défaut est dans WRAPPER_ASSIGNMENTS
, vous pouvez la voir dans source de functools .__dict__
de g
avec tous les éléments de f.__dict__
. (voir WRAPPER_UPDATES
dans le source)__wrapped__=f
sur g
La conséquence est que g
apparaît avec le même nom, le même document, le même nom de module et la même signature que f
. Le seul problème est que ce qui concerne la signature n’est pas vrai en réalité: 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 doc . Cela a des conséquences fâcheuses:
Signature.bind()
.Maintenant, il y a un peu de confusion entre functools.wraps
et les décorateurs, car un cas d'utilisation très fréquent pour les décorateurs en développement consiste à utiliser 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 pour écrire facilement des décorateurs, et makefun pour fournir un substitut préservant la signature pour @wraps
. Notez que makefun
repose sur le même truc éprouvé que la célèbre bibliothèque decorator
.
c'est le code source de 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)
Prérequis: Vous devez savoir utiliser les décorateurs et spécialement les wraps. Ce commentaire explique cela un peu clair ou ceci lien l'explique aussi plutôt bien.
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 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.
Cela équivaut à partial (update_wrapper, wrapped = wrapped, assigné = assigné, mis à jour = mis à jour).
Donc, le décorateur @wraps appelle en fait functools.partial (func [ * args] [ ** keywords]).
La définition de functools.partial () dit que
Partial () est utilisé pour l’application de fonctions partielles qui "gèle" une partie des arguments et/ou des mots-clés d’une fonction, ce qui donne 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.