Comprender los Decoradores en Python

PythonBeginner
Practicar Ahora

Introducción

En este laboratorio, obtendrá una comprensión exhaustiva de los decoradores (decorators) en Python, una característica potente para modificar o mejorar funciones y métodos. Comenzaremos introduciendo el concepto fundamental de los decoradores y explorando su uso básico a través de ejemplos prácticos.

Basándose en esta base, aprenderá a utilizar eficazmente functools.wraps para preservar metadatos importantes de la función decorada. Luego profundizaremos en decoradores específicos como el decorador property, entendiendo su papel en la gestión del acceso a atributos. Finalmente, el laboratorio aclarará las distinciones entre métodos de instancia (instance methods), métodos de clase (class methods) y métodos estáticos (static methods), demostrando cómo se utilizan los decoradores en estos contextos para controlar el comportamiento de los métodos dentro de las clases.

Este es un Guided Lab, que proporciona instrucciones paso a paso para ayudarte a aprender y practicar. Sigue las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel principiante con una tasa de finalización del 93%. Ha recibido una tasa de reseñas positivas del 100% por parte de los estudiantes.

Comprensión de los Decoradores Básicos

En este paso, introduciremos el concepto de decoradores y su uso básico. Un decorador es una función que toma otra función como argumento, le añade alguna funcionalidad y devuelve otra función, todo ello sin alterar el código fuente de la función original.

Primero, localice el archivo decorator_basics.py en el explorador de archivos del lado izquierdo del WebIDE. Haga doble clic para abrirlo. Escribiremos nuestro primer decorador en este archivo.

Copie y pegue el siguiente código en decorator_basics.py:

import datetime

def log_activity(func):
    """A simple decorator to log function calls."""
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

Analicemos este código:

  • Definimos una función decoradora log_activity que acepta una función func como su argumento.
  • Dentro de log_activity, definimos una función anidada wrapper. Esta función contendrá el nuevo comportamiento. Imprime un mensaje de registro (log), llama a la función original func e imprime otro mensaje de registro.
  • La función log_activity devuelve la función wrapper.
  • La sintaxis @log_activity encima de la función greet es un atajo para greet = log_activity(greet). Aplica nuestro decorador a la función greet.

Ahora, guarde el archivo (puede usar Ctrl+S o Cmd+S). Para ejecutar el script, abra la terminal integrada en la parte inferior del WebIDE y ejecute el siguiente comando:

python ~/project/decorator_basics.py

Verá la siguiente salida. Tenga en cuenta que la fecha y hora variarán.

Calling function 'greet' at 2023-10-27 10:30:00.123456
Hello, Alice!
Function 'greet' finished.

Function name: wrapper
Function docstring: None

Observe dos cosas en la salida. Primero, nuestra función greet ahora está envuelta con los mensajes de registro. Segundo, el nombre y la cadena de documentación (docstring) de la función han sido reemplazados por los de la función wrapper. Esto puede ser problemático para la depuración (debugging) y la introspección. En el siguiente paso, aprenderemos a solucionar esto.

Preservar Metadatos de Funciones con functools.wraps

En el paso anterior, observamos que decorar una función reemplaza sus metadatos originales (como __name__ y __doc__) con los metadatos de la función wrapper (envoltorio). El módulo functools de Python proporciona una solución para esto: el decorador wraps.

El decorador wraps se utiliza dentro de su propio decorador para copiar los metadatos de la función original a la función wrapper.

Modifiquemos nuestro código en decorator_basics.py. Abra el archivo en el WebIDE y actualícelo para usar functools.wraps.

import datetime
from functools import wraps

def log_activity(func):
    """A simple decorator to log function calls."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata again
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

Los únicos cambios son:

  1. Importamos wraps desde el módulo functools.
  2. Añadimos @wraps(func) justo encima de la definición de nuestra función wrapper.

Guarde el archivo y ejecútelo de nuevo desde la terminal:

python ~/project/decorator_basics.py

Ahora, la salida será diferente:

Calling function 'greet' at 2023-10-27 10:35:00.543210
Hello, Alice!
Function 'greet' finished.

Function name: greet
Function docstring: A simple function to greet someone.

Como puede ver, el nombre de la función se informa correctamente como greet, y su docstring original se conserva. Usar functools.wraps es una mejor práctica que hace que sus decoradores sean más robustos y profesionales.

Implementación de Atributos Gestionados con @property

Python proporciona varios decoradores integrados. Uno de los más útiles es @property, que permite convertir un método de clase en un "atributo gestionado" (managed attribute). Esto es ideal para añadir lógica, como validación o cálculo, al acceso de atributos sin cambiar la forma en que los usuarios interactúan con su clase.

Exploremos esto creando una clase Circle. Abra el archivo property_decorator.py en el explorador de archivos.

Copie y pegue el siguiente código en property_decorator.py:

import math

class Circle:
    def __init__(self, radius):
        ## The actual value is stored in a "private" attribute
        self._radius = radius

    @property
    def radius(self):
        """The radius property."""
        print("Getting radius...")
        return self._radius

    @radius.setter
    def radius(self, value):
        """The radius setter with validation."""
        print(f"Setting radius to {value}...")
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        """A read-only computed property for the area."""
        print("Calculating area...")
        return math.pi * self._radius ** 2

## --- Let's test our Circle class ---
c = Circle(5)

## Access the radius like a normal attribute (triggers the getter)
print(f"Initial radius: {c.radius}\n")

## Change the radius (triggers the setter)
c.radius = 10
print(f"New radius: {c.radius}\n")

## Access the computed area property
print(f"Circle area: {c.area:.2f}\n")

## Try to set an invalid radius (triggers the setter's validation)
try:
    c.radius = -2
except ValueError as e:
    print(f"Error: {e}")

En este código:

  • @property sobre el método radius define un "getter" (método de obtención). Se llama cuando se accede a c.radius.
  • @radius.setter define un "setter" (método de asignación) para la propiedad radius. Se llama cuando se asigna un valor, como c.radius = 10. Hemos añadido validación aquí para prevenir valores negativos.
  • El método area también usa @property pero no tiene un setter, lo que lo convierte en un atributo de solo lectura (read-only). Su valor se calcula cada vez que se accede a él.

Guarde el archivo y ejecútelo desde la terminal:

python ~/project/property_decorator.py

Debería ver la siguiente salida, que demuestra cómo se invocan automáticamente la lógica del getter, setter y la validación:

Getting radius...
Initial radius: 5

Setting radius to 10...
Getting radius...
New radius: 10

Calculating area...
Circle area: 314.16

Setting radius to -2...
Error: Radius cannot be negative

Diferenciación entre Métodos de Instancia, de Clase y Estáticos

En las clases de Python, los métodos pueden estar ligados a una instancia, a la clase, o no estar ligados en absoluto. Se utilizan decoradores para definir estos diferentes tipos de métodos.

  • Métodos de Instancia: El tipo por defecto. Reciben la instancia como primer argumento, convencionalmente llamado self. Operan sobre datos específicos de la instancia.
  • Métodos de Clase: Marcados con @classmethod. Reciben la clase como primer argumento, convencionalmente llamado cls. Operan sobre datos a nivel de clase y a menudo se utilizan como constructores alternativos.
  • Métodos Estáticos: Marcados con @staticmethod. No reciben ningún primer argumento especial. Son esencialmente funciones regulares nombradas dentro de una clase y no pueden acceder al estado de la instancia o de la clase.

Veamos los tres en acción. Abra el archivo class_methods.py en el explorador de archivos.

Copie y pegue el siguiente código en class_methods.py:

class MyClass:
    class_variable = "I am a class variable"

    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

    ## 1. Instance Method
    def instance_method(self):
        print("\n--- Calling Instance Method ---")
        print(f"Can access instance data: self.instance_variable = '{self.instance_variable}'")
        print(f"Can access class data: self.class_variable = '{self.class_variable}'")

    ## 2. Class Method
    @classmethod
    def class_method(cls):
        print("\n--- Calling Class Method ---")
        print(f"Can access class data: cls.class_variable = '{cls.class_variable}'")
        ## Note: Cannot access instance_variable without an instance
        print("Cannot access instance data directly.")

    ## 3. Static Method
    @staticmethod
    def static_method(a, b):
        print("\n--- Calling Static Method ---")
        print("Cannot access instance or class data directly.")
        print(f"Just a utility function: {a} + {b} = {a + b}")

## --- Let's test the methods ---
## Create an instance of the class
my_instance = MyClass("I am an instance variable")

## Call the instance method (requires an instance)
my_instance.instance_method()

## Call the class method (can be called on the class or an instance)
MyClass.class_method()
my_instance.class_method() ## Also works

## Call the static method (can be called on the class or an instance)
MyClass.static_method(10, 5)
my_instance.static_method(20, 8) ## Also works

Guarde el archivo y ejecútelo desde la terminal:

python ~/project/class_methods.py

Examine la salida cuidadosamente. Demuestra claramente las capacidades y limitaciones de cada tipo de método.

--- Calling Instance Method ---
Can access instance data: self.instance_variable = 'I am an instance variable'
Can access class data: self.class_variable = 'I am a class variable'

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 10 + 5 = 15

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 20 + 8 = 28

Este ejemplo proporciona una referencia clara sobre cuándo usar cada tipo de método en función de si necesita acceso al estado de la instancia, al estado de la clase, o a ninguno de los dos.

Resumen

En este laboratorio, ha adquirido una comprensión práctica de los decoradores (decorators) en Python. Comenzó aprendiendo a crear y aplicar un decorador básico para añadir funcionalidad a una función. Luego vio la importancia de usar functools.wraps para preservar los metadatos de la función original, una práctica recomendada crucial para escribir decoradores limpios y mantenibles.

Además, exploró decoradores integrados (built-in) potentes. Aprendió a usar el decorador @property para crear atributos gestionados con lógica personalizada de getter y setter, lo que permite funcionalidades como la validación de entradas. Finalmente, distinguió entre métodos de instancia, métodos de clase (@classmethod) y métodos estáticos (@staticmethod), entendiendo cómo cada uno cumple un propósito diferente dentro de la estructura de una clase en función de su acceso al estado de la instancia y de la clase.