Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ejemplos Adicionales: Excepciones Personalizadas

🧭 Navegación:

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

  1. ¿Cuál es la ventaja de incluir atributos adicionales en las excepciones personalizadas?
  2. ¿Por qué es útil crear una jerarquía de excepciones en lugar de una sola clase de excepción?
  3. ¿Cómo puedes preservar la excepción original al convertir excepciones estándar en personalizadas?
  4. ¿Qué patrón utilizarías para añadir información contextual a tus excepciones?
  5. ¿Cómo implementarías un mecanismo de reintentos automáticos usando excepciones personalizadas?

🧭 Navegación: