O que functools.wraps fazer?
Pergunta
Em um comentário sobre esta responder a outra pergunta , alguém disse que eles não tinham certeza do que functools.wraps
estava fazendo. Então, eu estou fazendo esta pergunta de modo que haverá um registro dele em StackOverflow para futura referência:? O que functools.wraps
fazer, exatamente
Solução
Quando você usa um decorador, você está substituindo uma função com outro. Em outras palavras, se você tem um decorador
def logged(func):
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
então quando você diz
@logged
def f(x):
"""does some math"""
return x + x * x
é exatamente o mesmo que dizer
def f(x):
"""does some math"""
return x + x * x
f = logged(f)
e seu f
função é substituído pelo with_logging função. Infelizmente, isso significa que se você, em seguida, dizer
print(f.__name__)
ele irá imprimir with_logging
porque esse é o nome da sua nova função. Na verdade, se você olhar para o docstring para f
, será em branco porque with_logging
não tem docstring, e assim a docstring que você escreveu não vai estar mais lá. Além disso, se você olhar para o resultado pydoc para essa função, ele não será listado como tendo um x
argumento; em vez disso, vai ser listado como tomar *args
e **kwargs
porque é isso que with_logging preciso.
Se estiver usando um decorador sempre significou perder esta informação sobre uma função, que seria um problema sério. É por isso que temos functools.wraps
. Isso leva uma função usada em um decorador e adiciona a funcionalidade de copiar sobre o nome da função, DocString, lista de argumentos, etc. E uma vez que wraps
é em si um decorador, o código a seguir faz a coisa correta:
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'
Outras dicas
Eu muitas vezes usar classes, ao invés de funções, para os meus decoradores. Eu estava tendo alguns problemas com isso porque um objeto não terá todos os mesmos atributos que se espera de uma função. Por exemplo, um objeto não terá o __name__
atributo. Eu tive um problema específico com isso que era muito difícil de rastrear onde Django estava relatando o erro "objeto não tem nenhum atributo '__name__
'". Infelizmente, para decoradores de estilo classe, eu não acredito que @wrap irá fazer o trabalho. Tenho em vez criou uma classe decoradora de base assim:
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)
Esta proxies classe todo o atributo chama até a função que está sendo decorado. Então, agora você pode criar um decorador simples que verifica se 2 argumentos são especificados assim:
class process_login(DecBase):
def __call__(self, *args):
if len(args) != 2:
raise Exception("You can only specify two arguments")
return self.func(*args)
A partir do pitão 3,5 +:
@functools.wraps(f)
def g():
pass
É um alias para g = functools.update_wrapper(g, f)
. Ele faz exatamente três coisas:
- copia os
__module__
,__name__
,__qualname__
,__doc__
e atributos__annotations__
def
sobreg
. Esta lista padrão está emWRAPPER_ASSIGNMENTS
, você pode vê-lo no . - ele atualiza o
__dict__
deg
com todos os elementos def.__dict__
. (VejaWRAPPER_UPDATES
na fonte) - que define um novo atributo
__wrapped__=f
emg
A consequência é que aparece g
como tendo o mesmo nome, docstring, nome do módulo, e assinatura de f
. O único problema é a relativa à assinatura isso não é verdade: é justo que inspect.signature
segue invólucro cadeias por padrão. Você pode verificá-lo usando inspect.signature(g, follow_wrapped=False)
como explicado na doc . Isto tem consequências irritantes:
- o código de wrapper executará mesmo quando os argumentos fornecidos são inválidos.
- o código de wrapper não pode facilmente acessar um argumento usando seu nome, dos argumentos recebidos *, ** kwargs. De fato, um teria que lidar com todos os casos (posicional, palavra-chave padrão) e, portanto, usar algo como
Signature.bind()
.
Agora, há um pouco de confusão entre functools.wraps
e decoradores, porque um caso de uso muito freqüente para o desenvolvimento de decoradores é a funções envoltório. Mas ambos são conceitos totalmente independentes. Se você está interessado em compreender a diferença, eu implementado bibliotecas auxiliares para ambos: décopatch para gravação decoradores facilmente, e makefun para fornecer uma substituição de preservação da assinatura para @wraps
. Note-se que makefun
conta com o mesmo truque comprovado que a famosa biblioteca decorator
.
Este é o código-fonte sobre 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é-requisito: Você deve saber como usar decoradores e especialmente com wraps. Este comentário explica um pouco clara ou esse link também explica isso muito bem.
-
Sempre que usamos Para por exemplo: @wraps seguido por nossa própria função wrapper. De acordo com as informações dadas neste ligação , ele diz que
functools.wraps é função de conveniência para invocar update_wrapper () como uma função decorador, quando definindo uma função de invólucro.
é equivalente a parcial (update_wrapper, embrulhado = embrulhado, atribuído = atribuído, actualizado = actualizado).
Assim @wraps decorador realmente dá uma chamada para functools.partial (func [, * args] [, ** palavras-chave]).
A definição functools.partial () diz que
A parcial () é usado para aplicação de função parcial que “congela” alguma porção de argumentos e / ou palavras-chave, resultando em um novo objeto com uma assinatura simplificada de uma função. Por exemplo, parcial () pode ser usado para criar uma exigível que se comporta como a função int (), onde os padrões de argumento de base para dois:
>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18
O que me leva à conclusão de que, @wraps dá uma chamada para parcial () e passa a sua função de invólucro como um parâmetro para isso. A parcial () nos finais retorna a versão simplificada ou seja o objeto do que está dentro da função de invólucro e não a função de invólucro em si.
Em suma, functools.wraps é apenas uma função regular. Vamos considerar este oficial exemplo . Com a ajuda do código fonte , nós podemos ver mais detalhes sobre a execução e os passos que funcionam da seguinte forma:
- wraps (f) retorna um objeto, digamos, O1 . É um objeto da classe parcial
- O próximo passo é @ O1 ... que é a notação decorador em python. Significa
invólucro = O1 .__ chamada __ (invólucro)
Verificar a implementação de __ chamada __ , vemos que após esta etapa, (do lado esquerdo) embalagem se torna o objeto resultou por self.func (* self.args, * args, ** newkeywords ) Verificar a criação de O1 na __ nova __ , sabemos self.func é a função update_wrapper . Ele usa o parâmetro * args , do lado direito embalagem , como seu 1º parâmetro. Verificando a última etapa do update_wrapper , pode-se ver o lado direito embalagem é retornado, com alguns dos atributos modificados conforme necessário.