Preservar assinaturas de funções decorados
Pergunta
Suponha que eu tenha escrito um decorador que faz algo muito genérico. Por exemplo, pode converter todos os argumentos para um tipo específico, executar o log, implementar memoization, 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 decorado 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, há 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 documentação estão corretas:
>>> 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á errado. A informação "* args, ** kwargs" é quase inútil.
O que fazer? Eu posso pensar de duas soluções alternativas simples, mas falho:
1 - Incluir 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
Esta é ruim por causa da duplicação. A assinatura ainda não serão mostradas corretamente na documentação gerada automaticamente. É fácil atualizar a função e esquecer sobre como alterar a docstring, ou para fazer um erro de digitação. [ E sim, eu estou ciente do fato de que a docstring já duplica o corpo da função. Por favor, ignore esta; funny_function é apenas um exemplo aleatório. ]
2 - Não usar um decorador, ou usar um decorador para fins especiais 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
Esta multa trabalha para um conjunto de funções que tem assinatura idêntica, mas é inútil em geral. Como eu disse no início, eu quero ser capaz de usar decoradores inteiramente genericamente.
Eu estou procurando uma solução que é completamente geral e automática.
Assim, a pergunta é: existe uma maneira de editar a assinatura da função decorado depois de ter sido criado
Caso contrário, posso escrever um decorador que os extratos a assinatura da função e usa essa informação em vez de "kwargs *, kwargs **" ao construir a função decorado? Como faço para extrair essa informação? Como devo construir a função decorados -? Com ??exec
Todas as outras abordagens?
Solução
-
Instale decorador módulo:
$ pip install decorator
-
Adaptar definição de
args_as_ints()
:import decorator @decorator.decorator def args_as_ints(f, *args, **kwargs): args = [int(x) for x in args] kwargs = dict((k, int(v)) for k, v in kwargs.items()) return f(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x*y + 2*z print funny_function("3", 4.0, z="5") # 22 help(funny_function) # Help on function funny_function in module __main__: # # funny_function(x, y, z=3) # Computes x*y + 2*z
Python 3.4 +
functools.wraps()
de stdlib conservas assinaturas desde Python 3.4:
import functools
def args_as_ints(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args = [int(x) for x in args]
kwargs = dict((k, int(v)) for k, v in kwargs.items())
return func(*args, **kwargs)
return wrapper
@args_as_ints
def funny_function(x, y, z=3):
"""Computes x*y + 2*z"""
return x*y + 2*z
print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
# Computes x*y + 2*z
functools.wraps()
está disponível pelo menos desde Python 2.5 mas isso não acontece preservar a assinatura aí:
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
# Computes x*y + 2*z
Aviso:. *args, **kwargs
vez de x, y, z=3
Outras dicas
Esta é resolvido com functools
biblioteca padrão do Python e especificamente functools.wraps
função, que é projetado para " atualizar uma função wrapper para se parecer com a função envolto ". É um comportamento depende da versão Python, no entanto, como mostrado abaixo. Aplicado ao exemplo da questão, o código ficaria assim:
from functools import wraps
def args_as_ints(f):
@wraps(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
Quando executado em Python 3, este deve produzir a seguinte:
>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(x, y, z=3)
Computes x*y + 2*z
Sua única desvantagem é que, em Python 2 no entanto, não faz lista de argumentos da função de atualização. Quando executado em Python 2, que irá produzir:
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(*args, **kwargs)
Computes x*y + 2*z
Há um decorador módulo com decorador decorator
você pode usar :
@decorator
def args_as_ints(f, *args, **kwargs):
args = [int(x) for x in args]
kwargs = dict((k, int(v)) for k, v in kwargs.items())
return f(*args, **kwargs)
Em seguida, a assinatura e a ajuda do método é preservado:
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(x, y, z=3)
Computes x*y + 2*z
EDIT: J. F. Sebastian apontou que eu não modificam a função args_as_ints
- é corrigido agora
Dê uma olhada na decorador módulo - especificamente, a < a href = "http://www.phyast.pitt.edu/~micheles/python/documentation.html#decorator-is-a-decorator" rel = "noreferrer"> decorador decorador, que resolve este problema .
Segunda opção:
- Instale módulo wrapt:
$ easy_install wrapt
wrapt tem um bônus, preservar a assinatura de classe.
import wrapt
import inspect
@wrapt.decorator
def args_as_ints(wrapped, instance, args, kwargs):
if instance is None:
if inspect.isclass(wrapped):
# Decorator was applied to a class.
return wrapped(*args, **kwargs)
else:
# Decorator was applied to a function or staticmethod.
return wrapped(*args, **kwargs)
else:
if inspect.isclass(instance):
# Decorator was applied to a classmethod.
return wrapped(*args, **kwargs)
else:
# Decorator was applied to an instancemethod.
return wrapped(*args, **kwargs)
@args_as_ints
def funny_function(x, y, z=3):
"""Computes x*y + 2*z"""
return x * y + 2 * z
>>> funny_function(3, 4, z=5))
# 22
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(x, y, z=3)
Computes x*y + 2*z
Como comentado acima, em das jfs resposta ; Se você estiver preocupado com a assinatura em termos de aparência (help
e inspect.signature
), em seguida, usando functools.wraps
está perfeitamente bem.
Se você está preocupado com a assinatura em termos de comportamento (em particular TypeError
em caso de argumentos incompatibilidade), functools.wraps
não preservá-lo. Você deve preferir usar decorator
para isso, ou a minha generalização do seu motor principal, chamado makefun
.
from makefun import wraps
def args_as_ints(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("wrapper executes")
args = [int(x) for x in args]
kwargs = dict((k, int(v)) for k, v in kwargs.items())
return func(*args, **kwargs)
return wrapper
@args_as_ints
def funny_function(x, y, z=3):
"""Computes x*y + 2*z"""
return x*y + 2*z
print(funny_function("3", 4.0, z="5"))
# wrapper executes
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
# Computes x*y + 2*z
funny_function(0)
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)
Veja também este post sobre functools.wraps
.