Python
Tutorial Python: Decoradores
Aprende en este tutorial de Python cómo crear y usar decoradores para extender funciones de forma modular y eficiente.
Aprende Python y certifícateFundamento teórico de decoradores
En Python, los decoradores representan una poderosa herramienta de programación que permite modificar o extender el comportamiento de funciones o métodos sin alterar su código interno. Este patrón de diseño se basa en el concepto de funciones de orden superior - funciones que pueden recibir otras funciones como argumentos y/o devolver funciones como resultado.
Los decoradores son esencialmente una implementación del principio de envoltura (wrapping), donde una función es "envuelta" por otra que añade funcionalidad adicional. Esta técnica aprovecha la naturaleza de Python donde las funciones son objetos de primera clase, lo que significa que pueden ser manipuladas como cualquier otro objeto.
Concepto fundamental
Para entender los decoradores, debemos primero comprender que en Python:
- Las funciones son objetos que pueden asignarse a variables
- Las funciones pueden definirse dentro de otras funciones
- Las funciones pueden devolver otras funciones
Veamos un ejemplo sencillo que ilustra estos conceptos:
def saludar(nombre):
return f"Hola, {nombre}!"
# Asignar función a una variable
mi_funcion = saludar
# Usar la función a través de la variable
print(mi_funcion("Ana")) # Imprime: Hola, Ana!
Funciones anidadas y closures
Los decoradores aprovechan el concepto de closures (clausuras), que son funciones que "recuerdan" el entorno en el que fueron creadas. Esto permite a una función interna acceder a variables de la función externa que la contiene:
def crear_saludo(tipo):
def saludar(nombre):
return f"{tipo}, {nombre}!"
return saludar
saludo_formal = crear_saludo("Buenos días")
saludo_informal = crear_saludo("Hola")
print(saludo_formal("Profesor")) # Imprime: Buenos días, Profesor!
print(saludo_informal("Amigo")) # Imprime: Hola, Amigo!
En este ejemplo, crear_saludo
devuelve la función interna saludar
, que mantiene acceso a la variable tipo
incluso después de que la función externa haya terminado su ejecución.
Estructura básica de un decorador
Un decorador sigue esta estructura general:
def mi_decorador(funcion_original):
def funcion_wrapper(*args, **kwargs):
# Código que se ejecuta antes de la función original
print("Antes de llamar a la función")
# Llamada a la función original
resultado = funcion_original(*args, **kwargs)
# Código que se ejecuta después de la función original
print("Después de llamar a la función")
# Devolver el resultado
return resultado
# Devolver la función wrapper
return funcion_wrapper
Esta estructura permite:
- Recibir una función como argumento
- Definir una nueva función (wrapper) que añade comportamiento
- Devolver la función wrapper
Aplicación de decoradores
Python proporciona una sintaxis especial para aplicar decoradores usando el símbolo @
:
@mi_decorador
def saludar(nombre):
return f"Hola, {nombre}!"
Este código es equivalente a:
def saludar(nombre):
return f"Hola, {nombre}!"
saludar = mi_decorador(saludar)
La sintaxis con @
es más legible y expresa claramente la intención de decorar la función.
Preservando metadatos
Un aspecto importante a considerar es que cuando decoramos una función, la función resultante (wrapper) pierde los metadatos originales como el nombre, la documentación y la firma. Para solucionar esto, Python proporciona el decorador functools.wraps
:
from functools import wraps
def mi_decorador(funcion):
@wraps(funcion) # Preserva los metadatos de la función original
def wrapper(*args, **kwargs):
# Código adicional
return funcion(*args, **kwargs)
return wrapper
Casos de uso comunes
Los decoradores son especialmente útiles para implementar aspectos transversales (cross-cutting concerns) como:
- Registro (logging): Registrar información antes/después de llamadas a funciones
- Medición de tiempo: Calcular cuánto tarda una función en ejecutarse
- Validación: Verificar argumentos antes de ejecutar la función
- Caché: Almacenar resultados para evitar cálculos repetidos
- Control de acceso: Verificar permisos antes de permitir la ejecución
Por ejemplo, un decorador para medir el tiempo de ejecución podría verse así:
import time
from functools import wraps
def medir_tiempo(funcion):
@wraps(funcion)
def wrapper(*args, **kwargs):
inicio = time.time()
resultado = funcion(*args, **kwargs)
fin = time.time()
print(f"La función {funcion.__name__} tardó {fin - inicio:.4f} segundos")
return resultado
return wrapper
@medir_tiempo
def operacion_lenta():
time.sleep(1) # Simula una operación que tarda 1 segundo
return "Operación completada"
operacion_lenta() # Imprime: La función operacion_lenta tardó 1.0005 segundos
Fundamentos teóricos avanzados
Los decoradores en Python están relacionados con varios conceptos de programación funcional y patrones de diseño:
- Composición de funciones: Los decoradores permiten componer funciones de manera elegante
- Patrón Decorator: Implementan el patrón de diseño Decorator del libro "Gang of Four"
- Programación orientada a aspectos: Permiten separar aspectos transversales del código principal
- Metaprogramación: Modifican el comportamiento del programa en tiempo de ejecución
Múltiples decoradores
Es posible aplicar varios decoradores a una misma función. Se aplican de abajo hacia arriba:
@decorador1
@decorador2
def mi_funcion():
pass
Esto es equivalente a:
mi_funcion = decorador1(decorador2(mi_funcion))
El decorador más cercano a la definición de la función (decorador2
) se aplica primero, y luego se aplica el siguiente (decorador1
).
Los decoradores son una característica elegante y poderosa de Python que permite escribir código más limpio, modular y mantenible, siguiendo el principio DRY (Don't Repeat Yourself) al extraer funcionalidades comunes a múltiples funciones.
Crear un decorador simple
Ahora que comprendemos los fundamentos teóricos de los decoradores, vamos a crear nuestro primer decorador simple. Un decorador es esencialmente una función que envuelve a otra función para extender su comportamiento sin modificar su código interno.
Para crear un decorador básico, seguiremos un patrón de tres pasos:
- Definir una función que reciba otra función como argumento
- Crear una función interna (wrapper) que añada funcionalidad
- Devolver la función wrapper
Ejemplo básico: un decorador de registro (logger)
Comencemos con un ejemplo práctico: un decorador que registra cuándo se llama a una función.
def registrar_llamada(funcion):
def wrapper(*args, **kwargs):
print(f"Llamando a la función: {funcion.__name__}")
resultado = funcion(*args, **kwargs)
print(f"La función {funcion.__name__} ha terminado")
return resultado
return wrapper
Este decorador imprime un mensaje antes y después de ejecutar la función decorada. Veamos cómo aplicarlo:
@registrar_llamada
def sumar(a, b):
return a + b
# Probemos la función decorada
resultado = sumar(5, 3)
print(f"Resultado: {resultado}")
Al ejecutar este código, veremos:
Llamando a la función: sumar
La función sumar ha terminado
Resultado: 8
Observa cómo el decorador ha añadido comportamiento antes y después de la ejecución de la función sumar
, sin modificar su código interno.
Decorador de temporizador
Un caso de uso común es medir el tiempo que tarda una función en ejecutarse. Creemos un decorador para ello:
import time
def medir_tiempo(funcion):
def wrapper(*args, **kwargs):
tiempo_inicio = time.time()
resultado = funcion(*args, **kwargs)
tiempo_fin = time.time()
print(f"La función {funcion.__name__} tardó {tiempo_fin - tiempo_inicio:.6f} segundos")
return resultado
return wrapper
@medir_tiempo
def proceso_lento():
time.sleep(1) # Simulamos un proceso que tarda 1 segundo
print("Proceso completado")
proceso_lento()
Este decorador es útil para identificar cuellos de botella en nuestro código o simplemente para tener una idea del rendimiento de nuestras funciones.
Preservando metadatos con wraps
Un problema con los decoradores simples es que pierden los metadatos de la función original, como el nombre, la documentación y la firma. Esto puede causar confusión al depurar o generar documentación.
Para solucionar este problema, usamos el decorador wraps
del módulo functools
:
from functools import wraps
def mi_decorador(funcion):
@wraps(funcion) # Preserva los metadatos de la función original
def wrapper(*args, **kwargs):
print("Antes de la función")
resultado = funcion(*args, **kwargs)
print("Después de la función")
return resultado
return wrapper
@mi_decorador
def saludar(nombre):
"""Esta función saluda a alguien por su nombre."""
return f"¡Hola, {nombre}!"
# Verificamos que los metadatos se preservan
print(saludar.__name__) # Imprime: saludar (no wrapper)
print(saludar.__doc__) # Imprime: Esta función saluda a alguien por su nombre.
Siempre es recomendable usar @wraps
en tus decoradores para mantener la transparencia en tu código.
Decorador de validación
Los decoradores son excelentes para implementar validaciones antes de ejecutar una función. Veamos un ejemplo que valida los tipos de los argumentos:
from functools import wraps
def validar_tipos(tipo_a, tipo_b):
def decorador(funcion):
@wraps(funcion)
def wrapper(a, b):
if not isinstance(a, tipo_a) or not isinstance(b, tipo_b):
raise TypeError(f"Los argumentos deben ser de tipo {tipo_a.__name__} y {tipo_b.__name__}")
return funcion(a, b)
return wrapper
return decorador
@validar_tipos(int, int)
def dividir(a, b):
if b == 0:
raise ZeroDivisionError("No se puede dividir por cero")
return a / b
# Prueba correcta
print(dividir(10, 2)) # Imprime: 5.0
# Prueba con tipos incorrectos
try:
print(dividir("10", 2))
except TypeError as e:
print(f"Error: {e}") # Imprime: Error: Los argumentos deben ser de tipo int y int
Este decorador verifica que los argumentos sean del tipo esperado antes de ejecutar la función, lo que ayuda a detectar errores temprano.
Decorador de caché simple
Otro uso común de los decoradores es implementar un sistema de caché para evitar cálculos repetidos:
def cache_simple(funcion):
memoria = {}
@wraps(funcion)
def wrapper(*args):
# Usamos args como clave porque es inmutable (a diferencia de kwargs)
if args not in memoria:
memoria[args] = funcion(*args)
print(f"Calculando resultado para {args}")
else:
print(f"Usando resultado en caché para {args}")
return memoria[args]
return wrapper
@cache_simple
def fibonacci(n):
"""Calcula el n-ésimo número de Fibonacci."""
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Primera llamada (calcula y guarda en caché)
print(fibonacci(5)) # Calcula fibonacci(5), fibonacci(4), etc.
# Segunda llamada (usa valores en caché)
print(fibonacci(5)) # Usa el valor en caché directamente
Este decorador almacena los resultados de llamadas previas, lo que es especialmente útil para funciones recursivas o cálculos costosos.
Decorador con argumentos
También podemos crear decoradores que acepten argumentos propios, lo que aumenta su flexibilidad:
def repetir(veces):
def decorador(funcion):
@wraps(funcion)
def wrapper(*args, **kwargs):
for _ in range(veces):
resultado = funcion(*args, **kwargs)
return resultado
return wrapper
return decorador
@repetir(veces=3)
def saludar(nombre):
print(f"¡Hola, {nombre}!")
return nombre
saludar("María") # Imprime "¡Hola, María!" tres veces
Observa la estructura de tres niveles: una función que devuelve un decorador, que a su vez devuelve una función wrapper.
Aplicaciones prácticas
Los decoradores simples tienen numerosas aplicaciones en el desarrollo real:
- Registro de actividad: Registrar llamadas a funciones críticas
- Gestión de excepciones: Capturar y manejar errores de forma centralizada
- Control de acceso: Verificar permisos antes de ejecutar funciones
- Depuración: Añadir información de depuración en desarrollo
- Sincronización: Implementar bloqueos para acceso concurrente
Por ejemplo, un decorador para gestión de excepciones:
def manejar_excepciones(funcion):
@wraps(funcion)
def wrapper(*args, **kwargs):
try:
return funcion(*args, **kwargs)
except Exception as e:
print(f"Error en {funcion.__name__}: {str(e)}")
# Aquí podríamos registrar el error en un archivo o sistema
return None # O un valor por defecto apropiado
return wrapper
@manejar_excepciones
def dividir(a, b):
return a / b
# Prueba normal
print(dividir(10, 2)) # Imprime: 5.0
# Prueba con error
print(dividir(10, 0)) # Imprime: Error en dividir: division by zero
Este decorador captura cualquier excepción que ocurra en la función decorada, evitando que el programa se detenga abruptamente.
Buenas prácticas al crear decoradores
Al crear tus propios decoradores, considera estas recomendaciones:
- Usa
functools.wraps
para preservar metadatos - Maneja correctamente los argumentos variables (
*args
y**kwargs
) - Mantén los decoradores enfocados en una sola responsabilidad
- Documenta claramente el propósito y comportamiento del decorador
- Evita efectos secundarios que puedan sorprender a otros desarrolladores
Los decoradores simples son una herramienta poderosa que, cuando se usan correctamente, pueden hacer que tu código sea más limpio, modular y mantenible.
Ejercicios de esta lección Decoradores
Evalúa tus conocimientos de esta lección Decoradores con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Módulo math
Reto herencia
Excepciones
Introducción a Python
Reto variables
Funciones Python
Reto funciones
Módulo datetime
Reto acumulación
Reto estructuras condicionales
Polimorfismo
Módulo os
Reto métodos dunder
Diccionarios
Reto clases y objetos
Reto operadores
Operadores
Estructuras de control
Funciones lambda
Reto diccionarios
Reto función lambda
Encapsulación
Reto coleciones
Reto funciones auxiliares
Crear módulos y paquetes
Módulo datetime
Excepciones
Operadores
Diccionarios
Reto map, filter
Reto tuplas
Proyecto gestor de tareas CRUD
Tuplas
Variables
Tipos de datos
Conjuntos
Reto mixins
Módulo csv
Módulo json
Herencia
Análisis de datos de ventas con Pandas
Reto fechas y tiempo
Reto estructuras de iteración
Funciones
Reto comprehensions
Variables
Reto serialización
Módulo csv
Reto polimorfismo
Polimorfismo
Clases y objetos
Reto encapsulación
Estructuras de control
Importar módulos y paquetes
Módulo math
Funciones lambda
Reto excepciones
Listas
Reto archivos
Encapsulación
Reto conjuntos
Clases y objetos
Instalación de Python y creación de proyecto
Reto listas
Tipos de datos
Crear módulos y paquetes
Tuplas
Herencia
Reto acceso a sistema
Proyecto sintaxis calculadora
Importar módulos y paquetes
Clases y objetos
Módulo os
Listas
Conjuntos
Reto tipos de datos
Reto matemáticas
Módulo json
Todas las lecciones de Python
Accede a todas las lecciones de Python y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Python
Introducción
Instalación Y Creación De Proyecto
Introducción
Tema 2: Tipos De Datos, Variables Y Operadores
Introducción
Instalación De Python
Introducción
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Estructuras Control Iterativo
Sintaxis
Estructuras Control Condicional
Sintaxis
Testing Con Pytest
Sintaxis
Listas
Estructuras De Datos
Tuplas
Estructuras De Datos
Diccionarios
Estructuras De Datos
Conjuntos
Estructuras De Datos
Comprehensions
Estructuras De Datos
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Mixins Y Herencia Múltiple
Programación Orientada A Objetos
Métodos Especiales (Dunder Methods)
Programación Orientada A Objetos
Composición De Clases
Programación Orientada A Objetos
Funciones Lambda
Programación Funcional
Aplicación Parcial
Programación Funcional
Entrada Y Salida, Manejo De Archivos
Programación Funcional
Decoradores
Programación Funcional
Generadores
Programación Funcional
Paradigma Funcional
Programación Funcional
Composición De Funciones
Programación Funcional
Funciones Orden Superior Map Y Filter
Programación Funcional
Funciones Auxiliares
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Archivos Comprimidos
Entrada Y Salida Io
Entrada Y Salida Avanzada
Entrada Y Salida Io
Archivos Temporales
Entrada Y Salida Io
Contexto With
Entrada Y Salida Io
Módulo Csv
Biblioteca Estándar
Módulo Json
Biblioteca Estándar
Módulo Datetime
Biblioteca Estándar
Módulo Math
Biblioteca Estándar
Módulo Os
Biblioteca Estándar
Módulo Re
Biblioteca Estándar
Módulo Random
Biblioteca Estándar
Módulo Time
Biblioteca Estándar
Módulo Collections
Biblioteca Estándar
Módulo Sys
Biblioteca Estándar
Módulo Statistics
Biblioteca Estándar
Módulo Pickle
Biblioteca Estándar
Módulo Pathlib
Biblioteca Estándar
Importar Módulos Y Paquetes
Paquetes Y Módulos
Crear Módulos Y Paquetes
Paquetes Y Módulos
Entornos Virtuales (Virtualenv, Venv)
Entorno Y Dependencias
Gestión De Dependencias (Pip, Requirements.txt)
Entorno Y Dependencias
Python-dotenv Y Variables De Entorno
Entorno Y Dependencias
Acceso A Datos Con Mysql, Pymongo Y Pandas
Acceso A Bases De Datos
Acceso A Mongodb Con Pymongo
Acceso A Bases De Datos
Acceso A Mysql Con Mysql Connector
Acceso A Bases De Datos
Novedades Python 3.13
Características Modernas
Operador Walrus
Características Modernas
Pattern Matching
Características Modernas
Instalación Beautiful Soup
Web Scraping
Sintaxis General De Beautiful Soup
Web Scraping
Tipos De Selectores
Web Scraping
Web Scraping De Html
Web Scraping
Web Scraping Para Ciencia De Datos
Web Scraping
Autenticación Y Acceso A Recursos Protegidos
Web Scraping
Combinación De Selenium Con Beautiful Soup
Web Scraping
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el concepto y fundamento teórico de los decoradores en Python.
- Aprender a crear decoradores simples que envuelven funciones para añadir funcionalidades.
- Conocer la importancia de preservar metadatos usando functools.wraps.
- Aplicar decoradores para casos prácticos como registro, temporización, validación y caché.
- Entender cómo crear decoradores con argumentos y buenas prácticas en su uso.