Escribe tus propios decoradores de Python

Visión general

En el artículo Deep Dive Into Python Decorators, presenté el concepto de decoradores Python, demostré muchos decoradores geniales y expliqué cómo usarlos..

En este tutorial te mostraré cómo escribir tus propios decoradores. Como verás, escribir tus propios decoradores te da mucho control y habilita muchas capacidades. Sin decoradores, esas capacidades requerirían muchos repetitivos propensos a errores y repetitivos que saturan su código o mecanismos completamente externos como la generación de códigos..

Un resumen rápido si no sabes nada de decoradores. Un decorador es un llamable (función, método, clase u objeto con un llamada() método) que acepta una llamada como entrada y devuelve una llamada como salida. Típicamente, la llamada devuelta hace algo antes y / o después de llamar a la entrada que se puede llamar. Aplicas el decorador usando la @ sintaxis. Un montón de ejemplos próximamente ...

El decorador de Hello World

Empecemos con un '¡Hola mundo!' decorador. Este decorador reemplazará totalmente cualquier pieza decorativa decorada con una función que simplemente imprima "¡Hola mundo!".

python def hello_world (f): def decorado (* args, ** kwargs): imprime 'Hello World!' regreso decorado

Eso es. Vamos a verlo en acción y luego explicar las diferentes piezas y cómo funciona. Supongamos que tenemos la siguiente función que acepta dos números e imprime su producto:

Python def multiplica (x, y): imprime x * y

Si invocas, obtienes lo que esperas:

multiplicar (6, 7) 42

Vamos a decorarlo con nuestro Hola Mundo decorador anotando el multiplicar funcionar con @Hola Mundo.

python @hello_world def multiplica (x, y): imprime x * y

Ahora cuando llamas multiplicar con cualquier argumento (incluidos tipos de datos incorrectos o número de argumentos incorrectos), el resultado es siempre "¡Hola mundo!" impreso.

"python multiply (6, 7) Hello World!

multiplicar () hola mundo!

multiplica ('zzz') Hola Mundo! "

DE ACUERDO. ¿Como funciona? La función de multiplicación original se reemplazó completamente por la función decorada anidada dentro de Hola Mundo decorador. Si analizamos la estructura de la Hola Mundo Entonces el decorador verá que acepta la entrada F (que no se usa en este simple decorador), define una función anidada llamada decorado que acepta cualquier combinación de argumentos y argumentos de palabras clave (def decorado (* args, ** kwargs)), y finalmente devuelve el decorado función.

Función de escritura y decoradores de métodos.

No hay diferencia entre escribir una función y un método decorador. La definición de decorador será la misma. La entrada que se puede llamar será una función regular o un método enlazado.

Vamos a verificar eso. Aquí hay un decorador que solo imprime la entrada que se puede llamar y escriba antes de invocarla. Esto es muy típico de un decorador para realizar alguna acción y continuar invocando el llamable original..

python def print_callable (f): def decorado (* args, ** kwargs): imprimir f, tipo (f) return f (* args, ** kwargs) return decorado

Tenga en cuenta la última línea que invoca la entrada que se puede llamar de forma genérica y devuelve el resultado. Este decorador no es intrusivo en el sentido de que puede decorar cualquier función o método en una aplicación operativa, y la aplicación continuará funcionando porque la función decorada invoca al original y solo tiene un pequeño efecto secundario antes..

Vamos a verlo en acción. Decoraré tanto nuestra función de multiplicación como un método..

"python @print_callable def multiplica (x, y): imprime x * y

clase A (objeto): @print_callable def foo (self): print 'foo () here "

Cuando llamamos a la función y al método, se imprime el nombre y luego realizan su tarea original:

"Python se multiplica (6, 7) 42

A (). Foo () foo () aquí "

Decoradores Con Argumentos

Los decoradores también pueden tomar argumentos. Esta capacidad para configurar el funcionamiento de un decorador es muy poderosa y le permite utilizar el mismo decorador en muchos contextos.

Suponga que su código es demasiado rápido y su jefe le pide que lo reduzca un poco porque está haciendo que los otros miembros del equipo se vean mal. Escribamos un decorador que mida cuánto tiempo se está ejecutando una función, y si se ejecuta en menos de un cierto número de segundos t, esperará hasta que t segundos expiren y luego volverá.

Lo que es diferente ahora es que el decorador mismo toma un argumento t eso determina el tiempo de ejecución mínimo, y se pueden decorar diferentes funciones con diferentes tiempos de ejecución mínimos. Además, notará que al introducir argumentos decorativos, se requieren dos niveles de anidación:

"tiempo de importación de Python

def minimum_runtime (t): def decorado (f): def wrapper (args, ** kwargs): inicio = tiempo. tiempo () resultado = f (args, ** kwargs) runtime = time.time () - start si runtime < t: time.sleep(t - runtime) return result return wrapper return decorated"

Vamos a desempacar. El decorador en sí, la función. minimum_runtime toma un argumento t, lo que representa el tiempo de ejecución mínimo para el celo decorado. La entrada de llamada F fue "empujado hacia abajo" a los anidados decorado función, y los argumentos de entrada que se pueden llamar fueron "empujados hacia abajo" a otra función anidada envoltura.

La lógica real tiene lugar dentro de la envoltura función. Se registra la hora de inicio, la original llamable. F Se invoca con sus argumentos, y se almacena el resultado. Luego se verifica el tiempo de ejecución, y si es menor que el mínimo t luego duerme por el resto del tiempo y luego regresa.

Para probarlo, crearé un par de funciones que llaman a la multiplicación y las decoran con diferentes retrasos..

"python @minimum_runtime (1) def slow_multiply (x, y): multiplica (x, y)

@ minimum_runtime (3) def slower_multiply (x, y): multiplica (x, y) "

Ahora voy a llamar multiplicar directamente, así como las funciones más lentas y medir el tiempo.

"tiempo de importación de Python

funcs = [multiplicar, slow_multiply, slower_multiply] para f en funcs: start = time.time () f (6, 7) print f, time.time () - start "

Aquí está la salida:

llanura 42 1.59740447998e-05 42 1.00477004051 42 3.00489807129

Como puede ver, la multiplicación original casi no tomó tiempo, y las versiones más lentas se retrasaron de acuerdo con el tiempo de ejecución mínimo provisto.

Otro dato interesante es que la función decorada ejecutada es la envoltura, que tiene sentido si se sigue la definición de decorado. Pero eso podría ser un problema, especialmente si estamos tratando con decoradores de pila. La razón es que muchos decoradores también inspeccionan su entrada y pueden verificar su nombre, firma y argumentos. Las siguientes secciones explorarán este problema y brindarán consejos para las mejores prácticas..

Decoradores de objetos

También puede usar objetos como decoradores o devolver objetos de sus decoradores. El único requisito es que tengan un __llamada__() Método, por lo que son invocables. Aquí hay un ejemplo para un decorador basado en objetos que cuenta cuántas veces se llama a su función de destino:

clase de python Contador (objeto): def __init __ (self, f): self.f = f self.called = 0 def __call __ (self, * args, ** kwargs): self.called + = 1 return self.f (* args, ** kwargs)

Aquí está en acción:

"python @Counter def bbb (): imprime 'bbb'

bbb () bbb

bbb () bbb

bbb () bbb

imprimir bbb.called 3 "

Elegir entre decoradores basados ​​en funciones y basados ​​en objetos

Esto es principalmente una cuestión de preferencia personal. Las funciones anidadas y los cierres de funciones proporcionan toda la administración del estado que ofrecen los objetos. Algunas personas se sienten más en casa con clases y objetos..

En la siguiente sección, hablaré sobre decoradores que se comportan bien, y los decoradores basados ​​en objetos toman un poco de trabajo extra para comportarse bien.

Decoradores de buen comportamiento

Los decoradores de propósito general a menudo pueden ser apilados. Por ejemplo:

python @ decorator_1 @ decorator_2 def foo (): imprime 'foo () here'

Cuando se apilan decoradores, el decorador externo (decorator_1 en este caso) recibirá el reclamo devuelto por el decorador interno (decorator_2). Si decorator_1 depende de alguna manera del nombre, los argumentos o la cadena de documentos de la función original y decorator_2 se implementa de forma ingenua, entonces decorator_2 no verá la información correcta de la función original, sino que solo el llamable devuelto por decorator_2.

Por ejemplo, aquí hay un decorador que verifica que el nombre de la función de destino es todo en minúscula:

python def check_lowercase (f): def decorado (* args, ** kwargs): afirmar f.func_name == f.func_name.lower () f (* args, ** kwargs) volver decorado

Vamos a decorar una función con ella:

python @check_lowercase def Foo (): imprime 'Foo () aquí'

Llamar a Foo () da como resultado una aserción:

"plain In [51]: Foo () - AssertionError Traceback (última llamada más reciente)

en () ----> 1 Foo () en decorado (* args, ** kwargs) 1 def check_lowercase (f): 2 def decorado (* args, ** kwargs): ----> 3 confirma f.func_name == f.func_name.lower () 4 retorno decorado "Pero si apilamos el decorador ** check_lowercase ** sobre un decorador como ** hello_world ** que devuelve una función anidada llamada 'decorado' el resultado es muy diferente:" python @check_lowercase @hello_world def Foo (): imprimir ' Foo () aquí 'Foo () ¡Hola mundo! "El decorador ** check_lowercase ** no hizo ninguna afirmación porque no vio el nombre de la función' Foo '. Este es un problema grave. El comportamiento adecuado para un decorador es preservar la mayor cantidad posible de atributos de la función original. Veamos cómo se hace. Ahora crearé un decorador de shell que simplemente llama a su entrada llamable, pero conserva toda la información de la función de entrada: el nombre de la función, todos sus atributos (en caso de que un decorador interno agregue algunos atributos personalizados), y su cadena de documentación. "python def passthrough (f): def decorado (* args, ** kwargs): f (* args , ** kwargs) decorado .__ nombre__ = f .__ nombre__ decorado .__ nombre__ = f .__ módulo__ decorado .__ dict__ = f .__ dict__ decorado .__ doc__ = f .__ doc__ regreso decorado "Ahora, decoradores apilados encima del ** pasante ** decorador funcionará como si decoraran la función de destino directamente. "python @check_lowercase @passthrough def Foo (): print 'Foo () here" ### Usando el @wraps Decorator Esta funcionalidad es tan útil que la biblioteca estándar tiene un especial decorador en el módulo functools llamado ['wraps'] (https://docs.python.org/2/library/functools.html#functools.wraps) para ayudar a escribir decoradores adecuados que funcionen bien con otros decoradores. Simplemente decoras dentro de tu decorador la función devuelta con ** @ wraps (f) **. Vea cuánto más conciso se ve ** passthrough ** cuando se usa ** wraps **: "python from functools import wraps def passthrough (f): @wraps (f) def decorado (* args, ** kwargs): f (* args, ** kwargs) volver decorado "Recomiendo siempre usarlo a menos que su decorador esté diseñado para modificar algunos de estos atributos. ## Decoradores de la clase de escritura Los decoradores de la clase se introdujeron en Python 3.0. Operan en una clase entera. Se invoca un decorador de clase cuando se define una clase y antes de crear cualquier instancia. Eso le permite al decorador de la clase modificar casi todos los aspectos de la clase. Normalmente agregarás o decorarás múltiples métodos. Comencemos con un ejemplo sofisticado: supongamos que tienes una clase llamada 'AwesomeClass' con un montón de métodos públicos (métodos cuyo nombre no comienza con un guión bajo como __init__) y tienes una clase de prueba basada en unittests llamada 'AwesomeClassTest '. AwesomeClass no solo es impresionante, sino que también es muy crítico, y usted quiere asegurarse de que si alguien agrega un nuevo método a AwesomeClass, también agregue un método de prueba correspondiente a AwesomeClassTest. Aquí está AwesomeClass: "clase de python AwesomeClass: def awesome_1 (self): return 'awesome!' def awesome_2 (self): return 'awesome! awesome! "Aquí está el AwesomeClassTest:" python from unittest import TestCase, clase principal AwesomeClassTest (TestCase): def test_awesome_1 (self): r = AwesomeClass (). awesome_1 () self.assertEqual ('awesome!', r) def test_awesome_2 (self): r = AwesomeClass (). awesome_2 () self.assertEqual ('awesome! awesome!', r) if __name__ == '__main__': main () "Ahora, Si alguien agrega un método ** awesome_3 ** con un error, las pruebas seguirán pasándose porque no hay ninguna prueba que llame ** awesome_3 **. ¿Cómo puede asegurarse de que siempre haya un método de prueba para cada método público? Bueno, escribes un decorador de clase, por supuesto. El decorador de clase @ensure_tests decorará el AwesomeClassTest y se asegurará de que cada método público tenga un método de prueba correspondiente. "Python def asegúrese_tests (cls, target_class): test_methods = [m for m in cls .__ dict__ if m.startswith ('test_' )] public_methods = [k para k, v en target_class .__ dict __. items () si se puede llamar (v) y no k.startswith ('_')] # Eliminar 'test_' prefijo de los nombres de los métodos de prueba test_methods = [m [5 :] para m en test_methods] if set (test_methods)! = set (public_methods): raise RuntimeError ('¡No coinciden los métodos de prueba / pública!') return cls "Esto se ve bastante bien, pero hay un problema. Los decoradores de clase solo aceptan un argumento: la clase decorada. El decorador garantizar_tests necesita dos argumentos: la clase y la clase objetivo. No pude encontrar una manera de tener decoradores de clase con argumentos similares a los decoradores funcionales. No tener miedo. Python tiene la función [functools.partial] (https://docs.python.org/2/library/functools.html#functools.partial) solo para estos casos ". Python @partial (asegurar_tests, target_class = AwesomeClass) clase AwesomeClassTest (TestCase): def test_awesome_1 (self): r = AwesomeClass (). Awesome_1 () self.assertEqual ('awesome!', R) def test_awesome_2 (self): r = AwesomeClass (). Awesome_2 () self.assertEqual (' ¡impresionante! ¡impresionante! ', r) si __name__ ==' __main__ ': main () "Ejecutar las pruebas resulta exitoso porque todos los métodos públicos, ** awesome_1 ** y ** awesome_2 **, tienen los métodos de prueba correspondientes, * * test_awesome_1 ** y ** test_awesome_2 **. "-------------------------------------- -------------------------------- Corrió 2 pruebas en 0.000s OK "Agreguemos un nuevo método ** awesome_3 ** sin una prueba correspondiente y ejecute las pruebas nuevamente ". clase Python AwesomeClass: def awesome_1 (self): return 'awesome!' def awesome_2 (self): return 'awesome! awesome!' def awesome_3 (self): return 'awesome! awesome! awesome! "Al ejecutar las pruebas nuevamente, se obtiene la siguiente salida:" Python3 a.py Traceback (última llamada más reciente): archivo "a.py", línea 25, en .