Python

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ícate

Fundamento 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:

  1. Recibir una función como argumento
  2. Definir una nueva función (wrapper) que añade comportamiento
  3. 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:

  1. Definir una función que reciba otra función como argumento
  2. Crear una función interna (wrapper) que añada funcionalidad
  3. 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.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende Python online

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

Python
Puzzle

Reto herencia

Python
Código

Excepciones

Python
Test

Introducción a Python

Python
Test

Reto variables

Python
Código

Funciones Python

Python
Puzzle

Reto funciones

Python
Código

Módulo datetime

Python
Test

Reto acumulación

Python
Código

Reto estructuras condicionales

Python
Código

Polimorfismo

Python
Test

Módulo os

Python
Test

Reto métodos dunder

Python
Código

Diccionarios

Python
Puzzle

Reto clases y objetos

Python
Código

Reto operadores

Python
Código

Operadores

Python
Test

Estructuras de control

Python
Puzzle

Funciones lambda

Python
Test

Reto diccionarios

Python
Código

Reto función lambda

Python
Código

Encapsulación

Python
Puzzle

Reto coleciones

Python
Proyecto

Reto funciones auxiliares

Python
Código

Crear módulos y paquetes

Python
Puzzle

Módulo datetime

Python
Puzzle

Excepciones

Python
Puzzle

Operadores

Python
Puzzle

Diccionarios

Python
Test

Reto map, filter

Python
Código

Reto tuplas

Python
Código

Proyecto gestor de tareas CRUD

Python
Proyecto

Tuplas

Python
Puzzle

Variables

Python
Puzzle

Tipos de datos

Python
Puzzle

Conjuntos

Python
Test

Reto mixins

Python
Código

Módulo csv

Python
Test

Módulo json

Python
Test

Herencia

Python
Test

Análisis de datos de ventas con Pandas

Python
Proyecto

Reto fechas y tiempo

Python
Proyecto

Reto estructuras de iteración

Python
Código

Funciones

Python
Test

Reto comprehensions

Python
Código

Variables

Python
Test

Reto serialización

Python
Proyecto

Módulo csv

Python
Puzzle

Reto polimorfismo

Python
Código

Polimorfismo

Python
Puzzle

Clases y objetos

Python
Código

Reto encapsulación

Python
Código

Estructuras de control

Python
Test

Importar módulos y paquetes

Python
Test

Módulo math

Python
Test

Funciones lambda

Python
Puzzle

Reto excepciones

Python
Código

Listas

Python
Puzzle

Reto archivos

Python
Proyecto

Encapsulación

Python
Test

Reto conjuntos

Python
Código

Clases y objetos

Python
Test

Instalación de Python y creación de proyecto

Python
Test

Reto listas

Python
Código

Tipos de datos

Python
Test

Crear módulos y paquetes

Python
Test

Tuplas

Python
Test

Herencia

Python
Puzzle

Reto acceso a sistema

Python
Proyecto

Proyecto sintaxis calculadora

Python
Proyecto

Importar módulos y paquetes

Python
Puzzle

Clases y objetos

Python
Puzzle

Módulo os

Python
Puzzle

Listas

Python
Test

Conjuntos

Python
Puzzle

Reto tipos de datos

Python
Código

Reto matemáticas

Python
Proyecto

Módulo json

Python
Puzzle

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

Python

Introducción

Instalación Y Creación De Proyecto

Python

Introducción

Tema 2: Tipos De Datos, Variables Y Operadores

Python

Introducción

Instalación De Python

Python

Introducción

Tipos De Datos

Python

Sintaxis

Variables

Python

Sintaxis

Operadores

Python

Sintaxis

Estructuras De Control

Python

Sintaxis

Funciones

Python

Sintaxis

Estructuras Control Iterativo

Python

Sintaxis

Estructuras Control Condicional

Python

Sintaxis

Testing Con Pytest

Python

Sintaxis

Listas

Python

Estructuras De Datos

Tuplas

Python

Estructuras De Datos

Diccionarios

Python

Estructuras De Datos

Conjuntos

Python

Estructuras De Datos

Comprehensions

Python

Estructuras De Datos

Clases Y Objetos

Python

Programación Orientada A Objetos

Excepciones

Python

Programación Orientada A Objetos

Encapsulación

Python

Programación Orientada A Objetos

Herencia

Python

Programación Orientada A Objetos

Polimorfismo

Python

Programación Orientada A Objetos

Mixins Y Herencia Múltiple

Python

Programación Orientada A Objetos

Métodos Especiales (Dunder Methods)

Python

Programación Orientada A Objetos

Composición De Clases

Python

Programación Orientada A Objetos

Funciones Lambda

Python

Programación Funcional

Aplicación Parcial

Python

Programación Funcional

Entrada Y Salida, Manejo De Archivos

Python

Programación Funcional

Decoradores

Python

Programación Funcional

Generadores

Python

Programación Funcional

Paradigma Funcional

Python

Programación Funcional

Composición De Funciones

Python

Programación Funcional

Funciones Orden Superior Map Y Filter

Python

Programación Funcional

Funciones Auxiliares

Python

Programación Funcional

Reducción Y Acumulación

Python

Programación Funcional

Archivos Comprimidos

Python

Entrada Y Salida Io

Entrada Y Salida Avanzada

Python

Entrada Y Salida Io

Archivos Temporales

Python

Entrada Y Salida Io

Contexto With

Python

Entrada Y Salida Io

Módulo Csv

Python

Biblioteca Estándar

Módulo Json

Python

Biblioteca Estándar

Módulo Datetime

Python

Biblioteca Estándar

Módulo Math

Python

Biblioteca Estándar

Módulo Os

Python

Biblioteca Estándar

Módulo Re

Python

Biblioteca Estándar

Módulo Random

Python

Biblioteca Estándar

Módulo Time

Python

Biblioteca Estándar

Módulo Collections

Python

Biblioteca Estándar

Módulo Sys

Python

Biblioteca Estándar

Módulo Statistics

Python

Biblioteca Estándar

Módulo Pickle

Python

Biblioteca Estándar

Módulo Pathlib

Python

Biblioteca Estándar

Importar Módulos Y Paquetes

Python

Paquetes Y Módulos

Crear Módulos Y Paquetes

Python

Paquetes Y Módulos

Entornos Virtuales (Virtualenv, Venv)

Python

Entorno Y Dependencias

Gestión De Dependencias (Pip, Requirements.txt)

Python

Entorno Y Dependencias

Python-dotenv Y Variables De Entorno

Python

Entorno Y Dependencias

Acceso A Datos Con Mysql, Pymongo Y Pandas

Python

Acceso A Bases De Datos

Acceso A Mongodb Con Pymongo

Python

Acceso A Bases De Datos

Acceso A Mysql Con Mysql Connector

Python

Acceso A Bases De Datos

Novedades Python 3.13

Python

Características Modernas

Operador Walrus

Python

Características Modernas

Pattern Matching

Python

Características Modernas

Instalación Beautiful Soup

Python

Web Scraping

Sintaxis General De Beautiful Soup

Python

Web Scraping

Tipos De Selectores

Python

Web Scraping

Web Scraping De Html

Python

Web Scraping

Web Scraping Para Ciencia De Datos

Python

Web Scraping

Autenticación Y Acceso A Recursos Protegidos

Python

Web Scraping

Combinación De Selenium Con Beautiful Soup

Python

Web Scraping

Accede GRATIS a Python y certifícate

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.