Ejemplos Adicionales: Excepciones Personalizadas
🧭 Navegación:
- Volver a Excepciones Personalizadas
- Siguiente: Propagación de Excepciones
Ejemplos Prácticos de Excepciones Personalizadas
En esta sección, encontrarás ejemplos adicionales de excepciones personalizadas en contextos reales.
Ejemplo 1: Sistema de Validación de Datos
Este ejemplo muestra cómo crear una jerarquía de excepciones para validar datos de entrada:
class ValidacionError(Exception):
"""Clase base para errores de validación de datos."""
pass
class TipoInvalidoError(ValidacionError):
"""Se lanza cuando el tipo de dato no es el esperado."""
def __init__(self, valor, tipo_esperado):
self.valor = valor
self.tipo_esperado = tipo_esperado
self.tipo_recibido = type(valor).__name__
mensaje = f"Tipo inválido: se esperaba {tipo_esperado.__name__}, se recibió {self.tipo_recibido}"
super().__init__(mensaje)
class ValorFueraDeRangoError(ValidacionError):
"""Se lanza cuando un valor numérico está fuera del rango permitido."""
def __init__(self, valor, minimo=None, maximo=None):
self.valor = valor
self.minimo = minimo
self.maximo = maximo
if minimo is not None and maximo is not None:
mensaje = f"Valor {valor} fuera de rango: debe estar entre {minimo} y {maximo}"
elif minimo is not None:
mensaje = f"Valor {valor} demasiado pequeño: debe ser >= {minimo}"
elif maximo is not None:
mensaje = f"Valor {valor} demasiado grande: debe ser <= {maximo}"
else:
mensaje = f"Valor {valor} fuera de rango"
super().__init__(mensaje)
class FormatoInvalidoError(ValidacionError):
"""Se lanza cuando un valor no cumple con el formato esperado."""
def __init__(self, valor, patron=None, descripcion=None):
self.valor = valor
self.patron = patron
if descripcion:
mensaje = f"Formato inválido: {descripcion}"
elif patron:
mensaje = f"Formato inválido: '{valor}' no coincide con el patrón '{patron}'"
else:
mensaje = f"Formato inválido: '{valor}'"
super().__init__(mensaje)
class ValorRequeridoError(ValidacionError):
"""Se lanza cuando falta un valor requerido."""
def __init__(self, campo):
self.campo = campo
mensaje = f"Valor requerido: el campo '{campo}' es obligatorio"
super().__init__(mensaje)
# Funciones de validación
def validar_tipo(valor, tipo_esperado):
"""Valida que un valor sea del tipo esperado."""
if not isinstance(valor, tipo_esperado):
raise TipoInvalidoError(valor, tipo_esperado)
return valor
def validar_rango(valor, minimo=None, maximo=None):
"""Valida que un valor numérico esté dentro del rango especificado."""
if (minimo is not None and valor < minimo) or (maximo is not None and valor > maximo):
raise ValorFueraDeRangoError(valor, minimo, maximo)
return valor
def validar_formato(valor, patron, descripcion=None):
"""Valida que un valor cumpla con un patrón de formato."""
import re
if not re.match(patron, valor):
raise FormatoInvalidoError(valor, patron, descripcion)
return valor
def validar_requerido(valor, campo):
"""Valida que un valor requerido no sea None o vacío."""
if valor is None or (isinstance(valor, (str, list, dict)) and len(valor) == 0):
raise ValorRequeridoError(campo)
return valor
# Ejemplo de uso
def validar_usuario(datos):
"""Valida los datos de un usuario."""
try:
# Validar campos requeridos
validar_requerido(datos.get('nombre'), 'nombre')
validar_requerido(datos.get('email'), 'email')
# Validar tipos
validar_tipo(datos['nombre'], str)
validar_tipo(datos['email'], str)
if 'edad' in datos:
validar_tipo(datos['edad'], int)
# Validar rangos
if 'edad' in datos:
validar_rango(datos['edad'], 18, 120)
# Validar formatos
validar_formato(
datos['email'],
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
"El email debe tener un formato válido"
)
return True
except ValidacionError as e:
# Podemos manejar la excepción o dejarla propagar
print(f"Error de validación: {e}")
raise
# Prueba
try:
usuario = {
'nombre': 'Ana García',
'email': 'ana.garcia@ejemplo.com',
'edad': 25
}
validar_usuario(usuario)
print("Usuario válido")
except TipoInvalidoError as e:
print(f"Error de tipo: {e}")
print(f"Valor: {e.valor}, Tipo esperado: {e.tipo_esperado.__name__}, Tipo recibido: {e.tipo_recibido}")
except ValorFueraDeRangoError as e:
print(f"Error de rango: {e}")
print(f"Valor: {e.valor}, Rango: {e.minimo} - {e.maximo}")
except FormatoInvalidoError as e:
print(f"Error de formato: {e}")
print(f"Valor: {e.valor}, Patrón: {e.patron}")
except ValorRequeridoError as e:
print(f"Error de campo requerido: {e}")
print(f"Campo: {e.campo}")
except ValidacionError as e:
print(f"Error de validación general: {e}")
Ejemplo 2: Excepciones para una API de Base de Datos
Este ejemplo muestra cómo crear excepciones personalizadas para una capa de acceso a datos:
class BaseDatosError(Exception):
"""Clase base para errores relacionados con la base de datos."""
def __init__(self, mensaje, codigo=None, query=None):
self.codigo = codigo
self.query = query
mensaje_completo = mensaje
if codigo:
mensaje_completo += f" (Código: {codigo})"
super().__init__(mensaje_completo)
class ConexionError(BaseDatosError):
"""Error al conectar con la base de datos."""
def __init__(self, mensaje, host=None, puerto=None, codigo=None):
self.host = host
self.puerto = puerto
mensaje_completo = mensaje
if host:
mensaje_completo += f" (Host: {host}"
if puerto:
mensaje_completo += f", Puerto: {puerto}"
mensaje_completo += ")"
super().__init__(mensaje_completo, codigo)
class ConsultaError(BaseDatosError):
"""Error al ejecutar una consulta SQL."""
pass
class DatoNoEncontradoError(BaseDatosError):
"""No se encontró el dato solicitado."""
def __init__(self, tabla, condiciones, codigo=None, query=None):
self.tabla = tabla
self.condiciones = condiciones
mensaje = f"No se encontró el registro en '{tabla}' con condiciones: {condiciones}"
super().__init__(mensaje, codigo, query)
class DuplicadoError(BaseDatosError):
"""Se intentó insertar un registro duplicado."""
def __init__(self, tabla, campo, valor, codigo=None, query=None):
self.tabla = tabla
self.campo = campo
self.valor = valor
mensaje = f"Registro duplicado en '{tabla}': {campo}='{valor}'"
super().__init__(mensaje, codigo, query)
class IntegridadError(BaseDatosError):
"""Error de integridad referencial."""
def __init__(self, mensaje, tabla=None, campo=None, codigo=None, query=None):
self.tabla = tabla
self.campo = campo
mensaje_completo = mensaje
if tabla and campo:
mensaje_completo += f" (Tabla: {tabla}, Campo: {campo})"
super().__init__(mensaje_completo, codigo, query)
# Clase de acceso a datos que usa las excepciones
class BaseDatos:
def __init__(self, host, usuario, password, base_datos):
self.host = host
self.usuario = usuario
self.password = password
self.base_datos = base_datos
self.conexion = None
def conectar(self):
"""Establece conexión con la base de datos."""
try:
# Aquí iría el código real de conexión
# Por ejemplo, con psycopg2 para PostgreSQL:
# self.conexion = psycopg2.connect(
# host=self.host,
# user=self.usuario,
# password=self.password,
# dbname=self.base_datos
# )
# Simulamos una conexión exitosa
self.conexion = "Conexión simulada"
print(f"Conectado a {self.base_datos} en {self.host}")
return True
except Exception as e:
# Convertimos la excepción estándar en nuestra excepción personalizada
raise ConexionError(
f"Error al conectar a la base de datos: {str(e)}",
host=self.host,
codigo="DB_CONN_001"
) from e
def ejecutar_consulta(self, query, parametros=None):
"""Ejecuta una consulta SQL."""
if not self.conexion:
raise ConexionError("No hay conexión activa a la base de datos")
try:
# Aquí iría el código real de ejecución de consulta
# Por ejemplo:
# cursor = self.conexion.cursor()
# cursor.execute(query, parametros)
# resultados = cursor.fetchall()
# return resultados
# Simulamos diferentes escenarios según la consulta
if "SELECT" in query and "WHERE id = 999" in query:
# Simular registro no encontrado
raise DatoNoEncontradoError(
"usuarios",
"id = 999",
codigo="DB_NOT_FOUND_001",
query=query
)
elif "INSERT" in query and "usuarios" in query:
# Simular error de duplicado
if parametros and "usuario@ejemplo.com" in str(parametros):
raise DuplicadoError(
"usuarios",
"email",
"usuario@ejemplo.com",
codigo="DB_DUP_001",
query=query
)
elif "DELETE" in query and "productos" in query:
# Simular error de integridad referencial
raise IntegridadError(
"No se puede eliminar el producto porque tiene pedidos asociados",
tabla="productos",
campo="id",
codigo="DB_FK_001",
query=query
)
# Simulamos una ejecución exitosa para otras consultas
print(f"Consulta ejecutada: {query}")
if "SELECT" in query:
return [{"id": 1, "nombre": "Ejemplo"}]
else:
return True
except (DatoNoEncontradoError, DuplicadoError, IntegridadError):
# Re-lanzamos nuestras propias excepciones
raise
except Exception as e:
# Convertimos otras excepciones
raise ConsultaError(
f"Error al ejecutar consulta: {str(e)}",
codigo="DB_QUERY_001",
query=query
) from e
def obtener_usuario(self, usuario_id):
"""Obtiene un usuario por su ID."""
try:
query = f"SELECT * FROM usuarios WHERE id = {usuario_id}"
resultado = self.ejecutar_consulta(query)
if not resultado:
raise DatoNoEncontradoError(
"usuarios",
f"id = {usuario_id}",
query=query
)
return resultado[0]
except BaseDatosError:
# Re-lanzamos nuestras propias excepciones
raise
except Exception as e:
# Convertimos otras excepciones
raise BaseDatosError(f"Error inesperado: {str(e)}") from e
# Ejemplo de uso
try:
db = BaseDatos("localhost", "usuario", "contraseña", "mi_base_datos")
db.conectar()
# Intentar obtener un usuario que no existe
usuario = db.obtener_usuario(999)
print(f"Usuario encontrado: {usuario}")
except ConexionError as e:
print(f"Error de conexión: {e}")
if hasattr(e, 'host'):
print(f"Host: {e.host}")
except DatoNoEncontradoError as e:
print(f"Dato no encontrado: {e}")
print(f"Tabla: {e.tabla}, Condiciones: {e.condiciones}")
if e.query:
print(f"Query: {e.query}")
except DuplicadoError as e:
print(f"Dato duplicado: {e}")
print(f"Tabla: {e.tabla}, Campo: {e.campo}, Valor: {e.valor}")
except IntegridadError as e:
print(f"Error de integridad: {e}")
if hasattr(e, 'tabla') and e.tabla:
print(f"Tabla: {e.tabla}, Campo: {e.campo}")
except BaseDatosError as e:
print(f"Error de base de datos: {e}")
if hasattr(e, 'codigo') and e.codigo:
print(f"Código de error: {e.codigo}")
Ejemplo 3: Excepciones para un Sistema de Archivos
Este ejemplo muestra cómo crear excepciones personalizadas para operaciones con archivos:
class ArchivoError(Exception):
"""Clase base para errores relacionados con archivos."""
def __init__(self, mensaje, ruta=None):
self.ruta = ruta
mensaje_completo = mensaje
if ruta:
mensaje_completo += f" (Ruta: {ruta})"
super().__init__(mensaje_completo)
class ArchivoNoEncontradoError(ArchivoError):
"""El archivo no existe."""
def __init__(self, ruta):
super().__init__("Archivo no encontrado", ruta)
class ArchivoNoAccesibleError(ArchivoError):
"""No se tiene permiso para acceder al archivo."""
def __init__(self, ruta, operacion=None):
self.operacion = operacion
mensaje = "Archivo no accesible"
if operacion:
mensaje += f" para {operacion}"
super().__init__(mensaje, ruta)
class FormatoArchivoError(ArchivoError):
"""El formato del archivo no es válido."""
def __init__(self, ruta, formato_esperado=None, detalle=None):
self.formato_esperado = formato_esperado
self.detalle = detalle
mensaje = "Formato de archivo inválido"
if formato_esperado:
mensaje += f", se esperaba {formato_esperado}"
if detalle:
mensaje += f": {detalle}"
super().__init__(mensaje, ruta)
class ArchivoCorruptoError(ArchivoError):
"""El archivo está corrupto o dañado."""
def __init__(self, ruta, detalle=None):
self.detalle = detalle
mensaje = "Archivo corrupto o dañado"
if detalle:
mensaje += f": {detalle}"
super().__init__(mensaje, ruta)
# Funciones que utilizan las excepciones
def leer_archivo(ruta, modo='r'):
"""Lee el contenido de un archivo."""
import os
# Verificar si el archivo existe
if not os.path.exists(ruta):
raise ArchivoNoEncontradoError(ruta)
# Verificar si se puede leer
if not os.access(ruta, os.R_OK):
raise ArchivoNoAccesibleError(ruta, "lectura")
try:
with open(ruta, modo) as archivo:
return archivo.read()
except PermissionError:
raise ArchivoNoAccesibleError(ruta, "lectura")
except UnicodeDecodeError:
raise FormatoArchivoError(ruta, "texto", "Error de codificación")
except Exception as e:
raise ArchivoError(f"Error al leer archivo: {str(e)}", ruta) from e
def leer_json(ruta):
"""Lee un archivo JSON."""
import json
try:
contenido = leer_archivo(ruta)
return json.loads(contenido)
except json.JSONDecodeError as e:
raise FormatoArchivoError(ruta, "JSON", str(e)) from e
except ArchivoError:
# Re-lanzar excepciones de archivo
raise
except Exception as e:
raise ArchivoError(f"Error inesperado: {str(e)}", ruta) from e
def leer_csv(ruta, delimitador=','):
"""Lee un archivo CSV."""
import csv
try:
contenido = leer_archivo(ruta)
filas = []
for linea in contenido.splitlines():
if not linea.strip():
continue
filas.append(linea.split(delimitador))
return filas
except ArchivoError:
# Re-lanzar excepciones de archivo
raise
except Exception as e:
raise FormatoArchivoError(ruta, "CSV", str(e)) from e
# Ejemplo de uso
def procesar_archivo_configuracion():
"""Procesa un archivo de configuración."""
try:
# Intentar leer un archivo JSON
config = leer_json("config.json")
print(f"Configuración cargada: {config}")
return config
except ArchivoNoEncontradoError as e:
print(f"Error: {e}")
print("Creando archivo de configuración por defecto...")
# Crear archivo de configuración por defecto
return {"configuracion": "por defecto"}
except ArchivoNoAccesibleError as e:
print(f"Error: {e}")
print(f"Operación: {e.operacion}")
print("Verifique los permisos del archivo")
return None
except FormatoArchivoError as e:
print(f"Error: {e}")
if e.formato_esperado:
print(f"Formato esperado: {e.formato_esperado}")
if e.detalle:
print(f"Detalle: {e.detalle}")
print("El archivo de configuración tiene un formato incorrecto")
return None
except ArchivoError as e:
print(f"Error general de archivo: {e}")
if e.ruta:
print(f"Ruta: {e.ruta}")
return None
Patrones Avanzados con Excepciones Personalizadas
Patrón: Excepciones con Contexto
Este patrón permite añadir información contextual a las excepciones:
class ContextoError(Exception):
"""Excepción base que mantiene información de contexto."""
def __init__(self, mensaje, **contexto):
self.contexto = contexto
mensaje_completo = mensaje
if contexto:
detalles = ", ".join(f"{k}={v}" for k, v in contexto.items())
mensaje_completo += f" [{detalles}]"
super().__init__(mensaje_completo)
# Ejemplo de uso
def procesar_datos(datos, contexto=None):
"""Procesa datos con información de contexto."""
contexto = contexto or {}
try:
# Código que puede fallar
resultado = datos["valor"] / datos.get("divisor", 0)
return resultado
except KeyError as e:
# Añadir contexto a la excepción
raise ContextoError(
f"Falta la clave requerida: {e}",
operacion="procesar_datos",
datos_recibidos=str(datos),
**contexto
) from e
except ZeroDivisionError as e:
# Añadir contexto a la excepción
raise ContextoError(
"División por cero",
operacion="procesar_datos",
datos_recibidos=str(datos),
**contexto
) from e
Patrón: Excepciones con Recuperación
Este patrón permite definir estrategias de recuperación dentro de las excepciones:
class RecuperableError(Exception):
"""Excepción base que incluye estrategias de recuperación."""
def __init__(self, mensaje):
self.recuperado = False
super().__init__(mensaje)
def intentar_recuperacion(self):
"""Método que las subclases deben implementar para recuperarse."""
raise NotImplementedError("Las subclases deben implementar este método")
class ConexionPerdidaError(RecuperableError):
"""Error de conexión perdida con estrategia de reconexión."""
def __init__(self, servicio, intentos_maximos=3):
self.servicio = servicio
self.intentos_maximos = intentos_maximos
self.intentos = 0
super().__init__(f"Conexión perdida con {servicio}")
def intentar_recuperacion(self):
"""Intenta reconectar al servicio."""
if self.intentos >= self.intentos_maximos:
return False
self.intentos += 1
print(f"Intento {self.intentos}/{self.intentos_maximos} de reconexión a {self.servicio}...")
# Aquí iría el código real de reconexión
# Simulamos éxito en el tercer intento
if self.intentos == 3:
print(f"Reconexión exitosa a {self.servicio}")
self.recuperado = True
return True
print("Reconexión fallida")
return False
# Ejemplo de uso
def ejecutar_con_recuperacion(funcion, *args, **kwargs):
"""Ejecuta una función con soporte para recuperación de errores."""
try:
return funcion(*args, **kwargs)
except RecuperableError as e:
print(f"Error recuperable: {e}")
while not e.recuperado:
if e.intentar_recuperacion():
# Reintentar la función original
print("Reintentando operación original...")
return funcion(*args, **kwargs)
if e.intentos >= e.intentos_maximos:
print("Se agotaron los intentos de recuperación")
raise
# Si llegamos aquí, la recuperación fue exitosa
print("Recuperación exitosa, reintentando operación...")
return funcion(*args, **kwargs)
Comprueba tu comprensión
- ¿Cuál es la ventaja de incluir atributos adicionales en las excepciones personalizadas?
- ¿Por qué es útil crear una jerarquía de excepciones en lugar de una sola clase de excepción?
- ¿Cómo puedes preservar la excepción original al convertir excepciones estándar en personalizadas?
- ¿Qué patrón utilizarías para añadir información contextual a tus excepciones?
- ¿Cómo implementarías un mecanismo de reintentos automáticos usando excepciones personalizadas?
🧭 Navegación:
- Volver a Excepciones Personalizadas
- Siguiente: Propagación de Excepciones