Suponha que eu tenha escrito um decorador que faz algo muito genérico. Por exemplo, ele pode converter todos os argumentos para um tipo específico, realizar registro, implementar memoização, etc.
Aqui está um exemplo:
def args_as_ints(f):
def g(*args, **kwargs):
args = [int(x) for x in args]
kwargs = dict((k, int(v)) for k, v in kwargs.items())
return f(*args, **kwargs)
return g
@args_as_ints
def funny_function(x, y, z=3):
"""Computes x*y + 2*z"""
return x*y + 2*z
>>> funny_function("3", 4.0, z="5")
22
Tudo bem até agora. No entanto, há um problema. A função decorada não retém a documentação da função original:
>>> help(funny_function)
Help on function g in module __main__:
g(*args, **kwargs)
Felizmente, existe uma solução alternativa:
def args_as_ints(f):
def g(*args, **kwargs):
args = [int(x) for x in args]
kwargs = dict((k, int(v)) for k, v in kwargs.items())
return f(*args, **kwargs)
g.__name__ = f.__name__
g.__doc__ = f.__doc__
return g
@args_as_ints
def funny_function(x, y, z=3):
"""Computes x*y + 2*z"""
return x*y + 2*z
Desta vez, o nome da função e a documentação estão corretos:
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(*args, **kwargs)
Computes x*y + 2*z
Mas ainda há um problema: a assinatura da função está errada. A informação "* args, ** kwargs" é quase inútil.
O que fazer? Posso pensar em duas soluções alternativas simples, mas falhas:
1 - Inclua a assinatura correta na docstring:
def funny_function(x, y, z=3):
"""funny_function(x, y, z=3) -- computes x*y + 2*z"""
return x*y + 2*z
Isso é ruim por causa da duplicação. A assinatura ainda não será mostrada corretamente na documentação gerada automaticamente. É fácil atualizar a função e esquecer de alterar a docstring ou cometer um erro de digitação. [ E sim, estou ciente do fato de que a docstring já duplica o corpo da função. Ignore isso; funny_function é apenas um exemplo aleatório. ]
2 - Não use um decorador, ou use um decorador especial para cada assinatura específica:
def funny_functions_decorator(f):
def g(x, y, z=3):
return f(int(x), int(y), z=int(z))
g.__name__ = f.__name__
g.__doc__ = f.__doc__
return g
Isso funciona bem para um conjunto de funções com assinatura idêntica, mas é inútil em geral. Como disse no início, quero poder usar decoradores de forma totalmente genérica.
Estou procurando uma solução totalmente geral e automática.
Portanto, a questão é: há uma maneira de editar a assinatura da função decorada depois que ela foi criada?
Caso contrário, posso escrever um decorador que extraia a assinatura da função e use essa informação em vez de "* kwargs, ** kwargs" ao construir a função decorada? Como faço para extrair essas informações? Como devo construir a função decorada - com exec?
Alguma outra abordagem?
inspect.Signature
para lidar com funções decoradas.Respostas:
Instale o módulo decorador :
Adapte a definição de
args_as_ints()
:Python 3.4+
functools.wraps()
de stdlib preserva assinaturas desde Python 3.4:functools.wraps()
está disponível pelo menos desde o Python 2.5, mas não preserva a assinatura lá:Observe: em
*args, **kwargs
vez dex, y, z=3
.fonte
functools.wraps()
já preserva assinaturas em Python 3.4+ (como dito na resposta). Você quer dizer que a configuraçãowrapper.__signature__
ajuda em versões anteriores? (quais versões você testou?)help()
mostra a assinatura correta no Python 3.4. Por que você acha quefunctools.wraps()
está quebrado e não IPython?help()
produz o resultado correto, a questão é que software deve ser consertado:functools.wraps()
ou IPython? Em qualquer caso, a atribuição manual__signature__
é, na melhor das hipóteses, uma solução alternativa - não é uma solução de longo prazo.inspect.getfullargspec()
ainda não retornou a assinatura adequada parafunctools.wraps
no python 3.4 e que você deve usar em seuinspect.signature()
lugar.Isso é resolvido com a biblioteca padrão do Python
functools
e, especificamentefunctools.wraps
, a função, que é projetada para " atualizar uma função de wrapper para se parecer com a função de wrapper ". Seu comportamento depende da versão do Python, entretanto, conforme mostrado abaixo. Aplicado ao exemplo da pergunta, o código ficaria assim:Quando executado em Python 3, isso produziria o seguinte:
Sua única desvantagem é que no Python 2, no entanto, ele não atualiza a lista de argumentos da função. Quando executado em Python 2, produzirá:
fonte
Há um módulo
decorator
decorador com decorador que você pode usar:Então, a assinatura e a ajuda do método são preservadas:
EDIT: JF Sebastian apontou que eu não modifiquei a
args_as_ints
função - ela foi corrigida agora.fonte
Dê uma olhada no módulo decorador - especificamente o decorador decorador, que resolve esse problema.
fonte
Segunda opçao:
$ easy_install wrapt
wrapt tem um bônus, preserva a assinatura da classe.
fonte
Conforme comentado acima na resposta de jfs ; se você está preocupado com a assinatura em termos de aparência (
help
einspect.signature
), então o usofunctools.wraps
é perfeitamente adequado.Se você está preocupado com a assinatura em termos de comportamento (em particular
TypeError
no caso de incompatibilidade de argumentos),functools.wraps
não a preserve. Você deve preferir usardecorator
para isso, ou minha generalização de seu motor principal, chamadomakefun
.Veja também este post sobre
functools.wraps
.fonte
inspect.getfullargspec
não é mantido por chamadafunctools.wraps
.