2.23. Decoradores

Un decorador es una porción de sintaxis que envuelve una función (o un método) dentro de otra función. El envoltorio puede añadir comportamiento antes y después de la llamada original, sustituir el valor de retorno o adjuntar metadatos. La forma es:

@wrapper
def f():
    ...

La línea @wrapper equivale a f = wrapper(f): la función f se construye con normalidad, luego se le entrega a wrapper, y el resultado se vuelve a vincular al nombre f.

Una función que entra en una caja "envoltorio" por la izquierda y sale por la derecha como una función decorada.

Un decorador recibe una función y devuelve una nueva función.

2.23.1. Decoradores de métodos integrados

Algunos decoradores vienen con Python y se usan dentro de los cuerpos de clase.

2.23.1.1. @property

Convierte un método en un atributo calculado. Quien llama accede a él como si fuera un atributo corriente (sin paréntesis), pero un método se ejecuta en cada lectura:

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius * self.radius

c = Circle(5)
print(c.area)             # 78.53975  -- no parentheses

Úsalo cuando un atributo que parece sencillo necesite un pequeño cálculo por detrás. Si el trabajo es costoso, prefiere un método normal: quien llama no espera que las lecturas de atributos sean lentas.

2.23.1.2. @classmethod

Define un método que recibe la clase como primer argumento en lugar de una instancia. El primer parámetro se nombra convencionalmente cls:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @classmethod
    def origin(cls):
        return cls(0, 0)

p = Point.origin()

Los métodos de clase son la forma estándar de proporcionar constructores alternativos: funciones de fábrica que devuelven una instancia construida de una manera no predeterminada.

2.23.1.3. @staticmethod

Define un método que no recibe ni la instancia ni la clase: es simplemente una función corriente que reside en el espacio de nombres de la clase por motivos organizativos:

class Temperature:
    @staticmethod
    def c_to_f(c):
        return c * 9 / 5 + 32

Temperature.c_to_f(100)   # 212.0

Úsalo con moderación; si una función realmente no tiene nada que ver con el estado de la clase, una función corriente a nivel de módulo suele ser más limpia.

2.23.2. Escribir un decorador personalizado

Un decorador es una función que recibe una función y devuelve una función. La forma mínima:

def log_calls(func):
    def wrapper(*args, **kwargs):
        print("calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(2, 3)

Salida:

calling add

El wrapper captura func en su clausura y le reenvía todo. *args / **kwargs le permite funcionar con cualquier función, no solo con las de dos argumentos. Este patrón es la base de decoradores más elaborados (medición de tiempos, almacenamiento en caché, reintento ante fallos), pero el núcleo es siempre el mismo: recibir una función y devolver una función.