O que o functools.wraps faz?

650

Em um comentário sobre essa resposta a outra pergunta , alguém disse que não tinha certeza do que functools.wrapsestava fazendo. Então, eu estou fazendo esta pergunta para que haja um registro no StackOverflow para referência futura: o que functools.wrapsfaz exatamente?

Eli Courtwright
fonte

Respostas:

1069

Quando você usa um decorador, substitui uma função por outra. 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 sua função fé substituído com a função with_logging. Infelizmente, isso significa que se você diz

print(f.__name__)

será impresso with_loggingporque esse é o nome da sua nova função. De fato, se você olhar para a sequência de caracteres f, ela ficará em branco porque with_loggingnão possui nenhuma sequência e, portanto, a sequência que você escreveu não estará mais lá. Além disso, se você observar o resultado do pydoc para essa função, ele não será listado como tendo um argumento x; em vez disso, será listado como take *argse **kwargsporque é isso que with_logging leva.

Se usar um decorador sempre significasse perder essas informações sobre uma função, seria um problema sério. É por isso que temos functools.wraps. Isso pega uma função usada em um decorador e adiciona a funcionalidade de copiar sobre o nome da função wraps, a sequência de documentos, a lista de argumentos etc. E, como ele próprio é 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'
Eli Courtwright
fonte
7
Sim, prefiro evitar o módulo decorador, pois o functools.wraps faz parte da biblioteca padrão e, portanto, não introduz outra dependência externa. Mas o módulo decorador realmente resolve o problema de ajuda, que, esperançosamente, funcionará.
Eli Courtwright
6
Aqui está um exemplo do que pode acontecer se você não usar bandagens: os testes do doctools podem desaparecer repentinamente. isso ocorre porque o doctools não consegue encontrar os testes em funções decoradas, a menos que algo como wraps () os copie.
precisa saber é o seguinte
88
por que precisamos functools.wrapspara este trabalho, não deveria ser apenas parte do padrão decorador? quando você não gostaria de usar o @wraps?
Wim
56
@ wim: Eu escrevi alguns decoradores que fazem sua própria versão @wrapspara executar vários tipos de modificação ou anotação nos valores copiados. Fundamentalmente, é uma extensão da filosofia Python que explícito é melhor que implícito e casos especiais não são especiais o suficiente para quebrar as regras. (O código é muito mais simples ea linguagem fácil de entender se @wrapsdevem ser fornecidos manualmente, em vez de usar algum tipo de mecanismo especial opt-out.)
ssokolow
35
@LucasMalor Nem todos os decoradores envolvem as funções que decoram. Alguns aplicam efeitos colaterais, como registrá-los em algum tipo de sistema de pesquisa.
23415 ssokolow
22

Costumo usar classes, em vez de funções, para meus decoradores. Eu estava tendo algum problema com isso porque um objeto não terá os mesmos atributos que se espera de uma função. Por exemplo, um objeto não terá o atributo __name__. Eu tive um problema específico com isso que era muito difícil de rastrear onde o Django estava relatando o erro "objeto não tem atributo ' __name__'". Infelizmente, para os decoradores de classe, não acredito que o @wrap faça o trabalho. Em vez disso, criei uma classe de decorador de base da seguinte forma:

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 classe proxies todos os atributos chama para a função que está sendo decorada. Portanto, agora você pode criar um decorador simples que verifique se 2 argumentos são especificados da seguinte maneira:

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
fonte
7
Como a documentação do @wrapsdiz, @wrapsé apenas uma função de conveniência para functools.update_wrapper(). No caso do decorador de classe, você pode chamar update_wrapper()diretamente do seu __init__()método. Assim, você não precisa criar DecBaseem tudo, você pode simplesmente incluir em __init__()de process_loginlinha: update_wrapper(self, func). Isso é tudo.
Fabiano
14

A partir do python 3.5+:

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

É um alias para g = functools.update_wrapper(g, f). Faz exatamente três coisas:

  • ele copia os __module__, __name__, __qualname__, __doc__, e __annotations__atributos de fon g. Esta lista padrão está dentro WRAPPER_ASSIGNMENTS, você pode vê-la na fonte de functools .
  • atualiza o __dict__de gcom todos os elementos de f.__dict__. (veja WRAPPER_UPDATESna fonte)
  • define um novo __wrapped__=fatributo emg

A conseqüência é que gparece ter o mesmo nome, sequência de caracteres, nome do módulo e assinatura que f. O único problema é que, com relação à assinatura, isso não é realmente verdade: é apenas o que inspect.signaturesegue as cadeias de wrapper por padrão. Você pode verificar usando inspect.signature(g, follow_wrapped=False)o explicado no documento . Isso tem consequências irritantes:

  • o código do wrapper será executado mesmo quando os argumentos fornecidos forem inválidos.
  • o código do wrapper não pode acessar facilmente um argumento usando seu nome, a partir dos * args, ** kwargs recebidos. De fato, seria necessário 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.wrapsdecoradores, porque um caso de uso muito frequente para desenvolver decoradores é agrupar funções. Mas ambos são conceitos completamente independentes. Se você estiver interessado em entender a diferença, implementei bibliotecas auxiliares para ambos: decopatch para escrever decoradores facilmente e makefun para fornecer um substituto que preserva a assinatura @wraps. Observe que se makefunbaseia no mesmo truque comprovado que a famosa decoratorbiblioteca.

smarie
fonte
3

Este é o código fonte sobre os envoltórios:

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
fonte
2
  1. Pré-requisito: você deve saber como usar decoradores e, especialmente, com envoltórios. Este comentário explica um pouco claro ou este link também explica muito bem.

  2. Sempre que usamos For, por exemplo: @wraps, seguido de nossa própria função de wrapper. De acordo com os detalhes fornecidos neste link , ele diz que

functools.wraps é uma função de conveniência para chamar update_wrapper () como um decorador de função, ao definir uma função de wrapper.

É equivalente a parcial (update_wrapper, empacotado = empacotado, atribuído = atribuído, atualizado = atualizado).

Então, o decorador @wraps na verdade chama uma função para functools.partial (func [, * args] [, ** keywords]).

A definição functools.partial () diz que

O parcial () é usado para aplicação de função parcial que "congela" uma parte dos argumentos e / ou palavras-chave de uma função, resultando em um novo objeto com uma assinatura simplificada. Por exemplo, parcial () pode ser usado para criar uma chamada que se comporta como a função int () em que o argumento base é o padrão de 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 o @wraps chama o parcial () e passa a função do wrapper como parâmetro para ele. O parcial () no final retorna a versão simplificada, ou seja, o objeto que está dentro da função de wrapper e não a função de wrapper em si.

3rdi
fonte
-4

Em resumo, functools.wraps é apenas uma função regular. Vamos considerar este exemplo oficial . Com a ajuda do código fonte , podemos ver mais detalhes sobre a implementação e as etapas em execução da seguinte maneira:

  1. wraps (f) retorna um objeto, digamos O1 . É um objeto da classe Parcial
  2. O próximo passo é @ O1 ... que é a notação do decorador em python. Isso significa

wrapper = O1 .__ chamar __ (wrapper)

Verificando a implementação de __call__ , vemos que, após esta etapa, o wrapper (do lado esquerdo) se torna o objeto resultante de self.func (* self.args, * args, ** newkeywords) Verificando a criação de O1 em __new__ , nós know self.func é a função update_wrapper . Ele usa o parâmetro * args , o invólucro do lado direito , como seu primeiro parâmetro. Verificando a última etapa do update_wrapper , é possível ver o retorno do wrapper do lado direito , com alguns dos atributos modificados conforme necessário.

Yong Yang
fonte