Kodokon kodokon.com

Decoradores y funciones de orden superior

Aprovecha las funciones como objetos de primera clase para escribir decoradores robustos, parametrizables y transparentes.

10 min · 3 preguntas

Abrir esta lección en Kodokon

En Python, una función es un objeto como cualquier otro: puedes asignarla a una variable, pasarla como argumento, devolverla. Una función de orden superior recibe o devuelve una función - sorted(data, key=...) es una de ellas. Añade los cierres (closures), donde una función interna captura las variables de la función que la envuelve y las mantiene vivas después de que esta termine, y ya tienes todos los ingredientes de los decoradores.

PYTHON
def make_multiplier(factor):
    def multiply(value):
        return value * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10))  # 20 30
Un cierre: multiply captura factor

Un decorador es una función que recibe una función y devuelve otra - normalmente un wrapper que añade comportamiento alrededor de la llamada. La sintaxis @timed encima de def slow_sum es puro azúcar sintáctico para slow_sum = timed(slow_sum). El wrapper acepta *args, kwargs para mantenerse agnóstico respecto a la firma, y devuelve** el resultado de la función decorada - olvidarlo es un error clásico que convierte cada valor de retorno en None.

PYTHON
import functools
import time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        delay = time.perf_counter() - start
        print(f"{func.__name__}: {delay:.4f}s")
        return result
    return wrapper

@timed
def slow_sum(n):
    return sum(range(n))

print(slow_sum(1_000_000))
Un decorador de cronometraje, listo para producción

Para parametrizar un decorador (@repeat(times=3)), hace falta un nivel más: una fábrica que recibe los parámetros y devuelve el decorador, que a su vez devuelve el wrapper. Tres funciones anidadas - ese es el precio de la flexibilidad. Si el estado se complica, una clase con un método __call__ suele ser más legible. Reserva los decoradores para los asuntos transversales: registro, caché, reintento, control de acceso. Un decorador que altera la lógica de negocio le oculta lo esencial a quien lee.

PYTHON
import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Hello {name}")

greet("Ada")
Decorador parametrizado: fábrica, decorador, wrapper

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿A qué equivale la sintaxis @timed colocada encima de def slow_sum?
    • timed(slow_sum())
    • slow_sum = timed(slow_sum)
    • slow_sum.timed = True
  2. ¿Qué problema resuelve @functools.wraps?
    • Acelera la llamada a la función decorada
    • Conserva el nombre y la docstring de la función decorada
    • Hace que el decorador sea compatible con los métodos de clase
  3. En @repeat(times=3), ¿qué devuelve la llamada repeat(times=3)?
    • El resultado de la función decorada
    • Un decorador, listo para recibir la función
    • Directamente el wrapper final
    • Una lista de tres resultados