Menu
Coddy logo textTech

Decoradores de clase

Parte de la sección Object Oriented Programming del Journey de Python de Coddy — lección 40 de 64.

Los decoradores de clases te permiten modificar o mejorar clases envolviéndolas con otra función. Funcionan como los decoradores de funciones, pero se aplican a clases enteras.

Aquí hay una clase simple sin decoración:

class Person:
    def __init__(self, name):
        self.name = name
        
    def greet(self):
        return f"Hello, my name is {self.name}"

Crea un decorador de clase que añada un nuevo método:

def add_farewell(cls):
    def farewell(self):
        return f"Goodbye from {self.name}"
    
    cls.farewell = farewell  # Añade el método a la clase
    return cls

Aplica el decorador a una clase usando @:

@add_farewell
class EnhancedPerson:
    def __init__(self, name):
        self.name = name
        
    def greet(self):
        return f"Hello, my name is {self.name}"

Ahora la clase tiene tanto los métodos originales como los añadidos:

person = EnhancedPerson("Alice")
print(person.greet())     # Hola, mi nombre es Alice
print(person.farewell())  # Adiós de parte de Alice

También puedes envolver un método existente — almacenando el original para que aún puedas llamarlo dentro del envoltorio. Esto es útil cuando quieres añadir comportamiento (como seguimiento o registro) alrededor de un método que la clase ya tiene:

def add_tracking(cls):
    original_greet = cls.greet  # Almacenar el método original

    def tracked_greet(self):
        print(f"greet was called")   # Comportamiento extra antes
        return original_greet(self)  # Llamar al método original

    cls.greet = tracked_greet  # Reemplazar el método en la clase
    return cls

@add_tracking
class TrackedPerson:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"Hello, my name is {self.name}"

Cuando llamas a greet ahora, el envoltorio se ejecuta primero, luego delega al original:

person = TrackedPerson("Alice")
print(person.greet())
# greet fue llamado
# Hello, my name is Alice

Los pasos clave para envolver un método existente son:
1. Guardar el método original: original = cls.method
2. Definir una nueva función que llame a original(self) más cualquier lógica adicional.
3. Asignar la nueva función de vuelta a la clase: cls.method = new_function
4. Devolver la clase modificada.

Punto clave: Los decoradores de clase toman una clase como entrada, la modifican o mejoran, y devuelven la clase modificada. Pueden añadir métodos, modificar atributos o envolver la funcionalidad existente. Usa @decorator_name encima de la definición de la clase para aplicarlos. Esto proporciona una forma limpia de extender la funcionalidad de la clase sin herencia.

challenge icon

Desafío

Intermedio

En este desafío, implementarás un decorador de clase que añade funcionalidad de seguimiento de llamadas a métodos.

  • decorator.py - Implementa el decorador de clase add_counter
  • calculator.py - Contiene las clases que utilizarán tu decorador

El archivo driver.py contiene escenarios de prueba extensos que validarán tu implementación frente a varios casos de uso, patrones de herencia y casos de borde. No necesitas modificar este archivo, pero examinarlo te ayudará a comprender el comportamiento esperado.

Hoja de referencia

Los decoradores de clases modifican o mejoran las clases envolviéndolas con otra función. Toman una clase como entrada, la modifican y devuelven la clase modificada.

Sintaxis básica de un decorador de clase:

def decorator_name(cls):
    # Modificar la clase
    return cls

@decorator_name
class MyClass:
    pass

Ejemplo - añadir un método a una clase:

def add_farewell(cls):
    def farewell(self):
        return f"Goodbye from {self.name}"
    
    cls.farewell = farewell  # Añadir el método a la clase
    return cls

@add_farewell
class EnhancedPerson:
    def __init__(self, name):
        self.name = name
        
    def greet(self):
        return f"Hello, my name is {self.name}"

person = EnhancedPerson("Alice")
print(person.greet())     # Hello, my name is Alice
print(person.farewell())  # Goodbye from Alice

Ejemplo - envolver un método existente (almacenar el original, luego reemplazar):

def loud_greet(cls):
    original_greet = cls.greet  # Almacenar el método original

    def new_greet(self):
        result = original_greet(self)  # Llamar al original
        return result.upper()          # Mejorar el resultado

    cls.greet = new_greet  # Reemplazar con la versión envuelta
    return cls

@loud_greet
class EnhancedPerson:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"Hello, my name is {self.name}"

person = EnhancedPerson("Alice")
print(person.greet())  # HELLO, MY NAME IS ALICE

Pasos clave para envolver un método existente:

original = cls.method_name — guardar el método original
def new_method(self): original(self) — llamarlo dentro del envoltorio (wrapper)
cls.method_name = new_method — reemplazar el método en la clase

Los decoradores de clases pueden añadir métodos, modificar atributos o envolver funcionalidades existentes sin utilizar la herencia.

Pruébalo tú mismo

from calculator import Calculator

# Manejador de casos de prueba exhaustivo
test_case = input()

if test_case == "basic_functionality":
    calc = Calculator()
    print(calc.add(5, 3))  # Debería imprimir 8
    print(calc.add(2, 7))  # Debería imprimir 9
    print(calc.subtract(10, 4))  # Debería imprimir 6
    print(calc.call_counts)  # Debería imprimir {'add': 2, 'subtract': 1}

elif test_case == "multiple_instances":
    calc1 = Calculator()
    calc2 = Calculator()
    
    print(calc1.add(5, 3))  # Debería imprimir 8
    print(calc2.add(2, 7))  # Debería imprimir 9
    print(calc1.subtract(10, 4))  # Debería imprimir 6
    
    # Ambas instancias deberían compartir el mismo call_counts
    print(calc1.call_counts)  # Debería imprimir {'add': 2, 'subtract': 1}
    print(calc2.call_counts)  # Debería imprimir {'add': 2, 'subtract': 1}
    print(calc1.call_counts is calc2.call_counts)  # Debería imprimir True

elif test_case == "method_call_order":
    calc = Calculator()
    
    print(calc.add(1, 2))  # Debería imprimir 3
    print(calc.subtract(5, 2))  # Debería imprimir 3
    print(calc.add(10, 20))  # Debería imprimir 30
    print(calc.add(7, 3))  # Debería imprimir 10
    print(calc.call_counts)  # Debería imprimir {'add': 3, 'subtract': 1}

elif test_case == "large_values":
    calc = Calculator()
    
    print(calc.add(1000000, 2000000))  # Debería imprimir 3000000
    print(calc.subtract(5000000, 2000000))  # Debería imprimir 3000000
    print(calc.call_counts)  # Debería imprimir {'add': 1, 'subtract': 1}

elif test_case == "negative_values":
    calc = Calculator()
    
    print(calc.add(-5, -3))  # Debería imprimir -8
    print(calc.subtract(-10, -4))  # Debería imprimir -6
    print(calc.call_counts)  # Debería imprimir {'add': 1, 'subtract': 1}

elif test_case == "float_values":
    calc = Calculator()
    
    print(calc.add(5.5, 3.2))  # Debería imprimir 8.7
    print(calc.subtract(10.5, 4.2))  # Debería imprimir 6.3
    print(calc.call_counts)  # Debería imprimir {'add': 1, 'subtract': 1}

elif test_case == "zero_values":
    calc = Calculator()
    
    print(calc.add(0, 0))  # Debería imprimir 0
    print(calc.subtract(0, 0))  # Debería imprimir 0
    print(calc.call_counts)  # Debería imprimir {'add': 1, 'subtract': 1}

elif test_case == "counter_reset":
    calc = Calculator()
    
    print(calc.add(5, 3))  # Debería imprimir 8
    print(calc.call_counts)  # Debería imprimir {'add': 1, 'subtract': 0}
    
    # Reiniciar el contador
    calc.call_counts = {"add": 0, "subtract": 0}
    
    print(calc.add(2, 7))  # Debería imprimir 9
    print(calc.call_counts)  # Debería imprimir {'add': 0, 'subtract': 0}

elif test_case == "counter_manipulation":
    calc = Calculator()
    
    print(calc.add(5, 3))  # Debería imprimir 8
    print(calc.call_counts)  # Debería imprimir {'add': 1, 'subtract': 0}
    
    # Manipular el contador directamente
    calc.call_counts["add"] = 100
    
    print(calc.add(2, 7))  # Debería imprimir 9
    print(calc.call_counts)  # Debería imprimir {'add': 101, 'subtract': 0}

elif test_case == "performance_test":
    calc = Calculator()
    
    # Realizar 1000 operaciones de suma
    for i in range(1000):
        calc.add(i, i+1)
    
    # Realizar 500 operaciones de resta
    for i in range(500):
        calc.subtract(i+10, i)
    
    print(calc.call_counts)  # Debería imprimir {'add': 1000, 'subtract': 500}
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Object Oriented Programming