2009-07-22 11 views
44

Digamos que tengo el siguiente:Cómo tira de decoradores de una función en Python

def with_connection(f): 
    def decorated(*args, **kwargs): 
     f(get_connection(...), *args, **kwargs) 
    return decorated 

@with_connection 
def spam(connection): 
    # Do something 

Quiero probar la función spam sin pasar por la molestia de crear una conexión (o lo que sea el decorador está haciendo)

Dado spam, ¿cómo le quito el decorador y obtengo la función subyacente "sin decorar"?

Respuesta

30

En el caso general, no se puede, porque

@with_connection 
def spam(connection): 
    # Do something 

es equivalente a

def spam(connection): 
    # Do something 

spam = with_connection(spam) 

que significa que el correo no deseado "original" podría incluso no existir. A (no muy bonita) piratear sería la siguiente:

def with_connection(f): 
    def decorated(*args, **kwargs): 
     f(get_connection(...), *args, **kwargs) 
    decorated._original = f 
    return decorated 

@with_connection 
def spam(connection): 
    # Do something 

spam._original(testcon) # calls the undecorated function 
+0

Si va a modificar el código para llamar a '_original', puede comentar el decorador. – eduffy

+2

@eduffy: Ese es el punto de la pregunta. – balpha

+0

Tienes razón ... No pensé en ello en un sentido de prueba. – eduffy

14

He aquí, FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:

orig_spam = spam.func_closure[0].cell_contents 

Editar: Para las funciones/métodos decoradas más de una vez y con decoradores más complicados puede intentar utilizar el siguiente código Se basa en el hecho de que las funciones decoradas son __name__d de forma diferente a la función original.

def search_for_orig(decorated, orig_name): 
    for obj in (c.cell_contents for c in decorated.__closure__): 
     if hasattr(obj, "__name__") and obj.__name__ == orig_name: 
      return obj 
     if hasattr(obj, "__closure__") and obj.__closure__: 
      found = search_for_orig(obj, orig_name) 
      if found: 
       return found 
    return None 

>>> search_for_orig(spam, "spam") 
<function spam at 0x027ACD70> 

No es a prueba de tontos. Fallará si el nombre de la función devuelto por un decorador es el mismo que el decorado. El orden de los controles hasattr() también es heurístico, hay cadenas de decoración que arrojan resultados incorrectos en cualquier caso. solución

+3

'func_closure' está siendo reemplazado por' __closure__' en 3.x y ya está en 2.6 –

+1

Lo vi cuando estaba jugando con funciones, pero se vuelve algo complicado si está usando más de un decorador en una función. Terminas llamando '.func_closure [0] .cell_contents' hasta que 'cell_contents es None'. Esperaba una solución más elegante. – Herge

+0

Probablemente no funcionará, si el decorador usa functools.wraps –

27

de balpha puede hacerse más generalizable con este meta-decorador:

def include_original(dec): 
    def meta_decorator(f): 
     decorated = dec(f) 
     decorated._original = f 
     return decorated 
    return meta_decorator 

A continuación, se puede decorar sus decoradores con @include_original, y cada uno tendrá una versión contrastable (sin decoración) escondido en su interior.

@include_original 
def shout(f): 
    def _(): 
     string = f() 
     return string.upper() 
    return _ 



@shout 
def function(): 
    return "hello world" 

>>> print function() 
HELLO_WORLD 
>>> print function._original() 
hello world 
+4

Ahora estamos hablando. metadecoración FTW. – brice

+1

¿Hay alguna forma de ampliar esto para que el original de nivel más profundo esté accesible en la función decorada más externa, por lo que no tengo que hacer ._original._original._original para una función envuelta en tres decoradores? – Sparr

+0

@jcdyer ¿Qué significa __decorate your decorators__? ¿Puedo hacer algo como \ @include_original (siguiente línea) \ @decorator_which_I_dont_control (línea siguiente) function_definition? – Harshdeep

2

El enfoque habitual para probar tales funciones es hacer que cualquier dependencia, como get_connection, se pueda configurar. Entonces puedes anularlo con un simulacro mientras pruebas. Básicamente lo mismo que la inyección de dependencia en el mundo de Java, pero mucho más simple gracias a la naturaleza dinámica de Pitones.

Código

porque podría ser algo como esto:

# decorator definition 
def with_connection(f): 
    def decorated(*args, **kwargs): 
     f(with_connection.connection_getter(), *args, **kwargs) 
    return decorated 

# normal configuration 
with_connection.connection_getter = lambda: get_connection(...) 

# inside testsuite setup override it 
with_connection.connection_getter = lambda: "a mock connection" 

Dependiendo de su código se puede encontrar un objeto mejor que el decorador para pegar la función de fábrica en. El problema de tenerlo en el decorador es que tendrías que recordar restaurarlo al valor anterior en el método de desmontaje.

6

En vez de hacer ..

def with_connection(f): 
    def decorated(*args, **kwargs): 
     f(get_connection(...), *args, **kwargs) 
    return decorated 

@with_connection 
def spam(connection): 
    # Do something 

orig_spam = magic_hack_of_a_function(spam) 

Se podía hacer ..

def with_connection(f): 
    .... 

def spam_f(connection): 
    ... 

spam = with_connection(spam_f) 

..que es todo la sintaxis @decorator hace - a continuación, puede acceder obviamente el original spam_f normalmente

+0

Buen enfoque, ¡tan inteligente! – laike9m

1

Añadir un decorador de no hacer nada:

def do_nothing(f): 
    return f 

Después de definir o importar with_connection pero antes de llegar a los métodos que utilizan como decorador, añadir:

if TESTING: 
    with_connection = do_nothing 

a continuación, si se establece la prueba global para with_connection Es cierto, se han sustituido por un decorador de no hacer nada.

16

Ha habido un poco de actualización para esta pregunta. Si está utilizando Python 3, puede usar la propiedad __wrapped__ que devuelve la función envuelta.

He aquí un ejemplo de Python Cookbook, 3rd edition

>>> @somedecorator 
>>> def add(x, y): 
...  return x + y 
... 
>>> orig_add = add.__wrapped__ 
>>> orig_add(3, 4) 
7 
>>> 

Ver la discusión para un uso más detallado de ese atributo.

+0

Python3 para la victoria! – funk

4

Ahora se puede utilizar el paquete de undecorated:

>>> from undecorated import undecorated 
>>> undecorated(spam) 

Pasa por la molestia de excavar a través de todas las capas de diferentes decoradores hasta que llega a la función de fondo y no requiere cambiar los decoradores originales. Funciona tanto en python2 como en python3.

Cuestiones relacionadas