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

Foi útil?

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__ de f sobre g. Esta lista padrão está em WRAPPER_ASSIGNMENTS, você pode vê-lo no .
  • ele atualiza o __dict__ de g com todos os elementos de f.__dict__. (Veja WRAPPER_UPDATES na fonte)
  • que define um novo atributo __wrapped__=f em g

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)
  1. 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.

  2. 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:

  1. wraps (f) retorna um objeto, digamos, O1 . É um objeto da classe parcial
  2. 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.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top