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?

Foi útil?

Solução

  1. Instale decorador módulo:

    $ pip install decorator
    
  2. 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:

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

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