Introducción
Contenido de la lección.
Objetivos de aprendizaje
- Objetivo 1
- Objetivo 2
- Objetivo 3
Objetivos de Aprendizaje
Al finalizar este tema, serás capaz de:
- Comprender la diferencia entre errores de sintaxis y excepciones en tiempo de ejecución
- Implementar bloques try-except-finally para manejo robusto de errores
- Crear y utilizar excepciones personalizadas para casos específicos de aplicación
- Aplicar mejores prácticas en el manejo de excepciones en proyectos profesionales
📚 Conceptos Fundamentales de Excepciones
Una excepción es un evento que ocurre durante la ejecución de un programa que interrumpe el flujo normal de las instrucciones. Python tiene muchas excepciones incorporadas como ValueError, TypeError, FileNotFoundError, etc. Cuando ocurre un error, Python 'levanta' (raise) una excepción. Si no se maneja, el programa termina y muestra un mensaje de error. El mecanismo de manejo de excepciones permite al programador anticipar errores potenciales y definir cómo responder a ellos. Esto es crucial para crear aplicaciones robustas que puedan continuar funcionando incluso cuando ocurren problemas inesperados.
# Intento de división por cero (genera ZeroDivisionError)
numero = 10
divisor = 0
resultado = numero / divisor
print(f'El resultado es: {resultado}')
ZeroDivisionError: division by zero
# Intento de sumar número y cadena
numero = 5
texto = 'Hola'
resultado = numero + texto
print(resultado)
TypeError: unsupported operand type(s) for +: 'int' and 'str'
🎯 Estructura Try-Except Básica
El bloque try-except es la estructura fundamental para manejar excepciones en Python. El código que puede generar una excepción se coloca en el bloque try, mientras que el código para manejar la excepción se coloca en el bloque except. Cuando ocurre una excepción en el bloque try, Python busca un bloque except que pueda manejar ese tipo específico de excepción. Si encuentra uno, ejecuta el código del bloque except en lugar de terminar el programa. Esta estructura permite que el programa continúe ejecutándose incluso después de encontrar errores.
try:
numero = 10
divisor = 0
resultado = numero / divisor
print(f'El resultado es: {resultado}')
except ZeroDivisionError:
print('Error: No se puede dividir por cero')
print('El programa continúa ejecutándose')
Error: No se puede dividir por cero El programa continúa ejecutándose
def operacion_segura(a, b):
try:
resultado = int(a) / int(b)
return f'Resultado: {resultado}'
except ValueError:
return 'Error: Valores no numéricos'
except ZeroDivisionError:
return 'Error: División por cero'
print(operacion_segura('10', '2'))
print(operacion_segura('abc', '5'))
print(operacion_segura('8', '0'))
Resultado: 5.0 Error: Valores no numéricos Error: División por cero
💡 Cláusula Finally y Manejo Avanzado
El bloque finally se ejecuta siempre, sin importar si ocurrió una excepción o no. Es ideal para código de limpieza como cerrar archivos o conexiones de base de datos. También podemos usar 'else' después de except, que se ejecuta solo si no ocurre ninguna excepción. La cláusula 'as' nos permite capturar el objeto excepción para acceder a información detallada del error. Además, podemos capturar múltiples excepciones en un solo bloque except usando tuplas.
def leer_archivo(nombre_archivo):
archivo = None
try:
archivo = open(nombre_archivo, 'r')
contenido = archivo.read()
print(f'Archivo leído exitosamente')
except FileNotFoundError as e:
print(f'Error: Archivo no encontrado - {e}')
except PermissionError:
print('Error: Sin permisos para leer el archivo')
else:
print('Operación completada sin errores')
finally:
if archivo:
archivo.close()
print('Recursos liberados')
leer_archivo('archivo_inexistente.txt')
Error: Archivo no encontrado - [Errno 2] No such file or directory: 'archivo_inexistente.txt' Recursos liberados
def procesar_datos(datos):
try:
# Simulamos operaciones que pueden fallar
if not datos:
raise ValueError('Los datos están vacíos')
resultado = sum(datos) / len(datos)
return resultado
except (TypeError, ValueError) as e:
print(f'Error de datos: {type(e).__name__}: {e}')
return None
except Exception as e:
print(f'Error inesperado: {type(e).__name__}: {e}')
return None
print(procesar_datos([1, 2, 3, 4]))
print(procesar_datos([]))
print(procesar_datos('no es lista'))
2.5 Error de datos: ValueError: Los datos están vacíos None Error de datos: TypeError: unsupported operand type(s) for +: 'int' and 'str' None
🔧 Creación de Excepciones Personalizadas
Las excepciones personalizadas permiten crear tipos de errores específicos para nuestras aplicaciones. Se crean heredando de la clase Exception o de sus subclases. Esto mejora la legibilidad del código y permite un manejo más específico de errores. Las excepciones personalizadas deben tener nombres descriptivos que terminen en 'Error' o 'Exception'. Pueden incluir atributos adicionales y métodos para proporcionar más información sobre el error. Es una práctica profesional crear jerarquías de excepciones para diferentes módulos de una aplicación.
class EdadInvalidaError(Exception):
'''Excepción levantada cuando la edad es inválida'''
def __init__(self, edad, mensaje='Edad fuera del rango válido'):
self.edad = edad
self.mensaje = mensaje
super().__init__(self.mensaje)
def validar_edad(edad):
if edad < 0 or edad > 150:
raise EdadInvalidaError(edad, f'La edad {edad} no es válida')
return f'Edad válida: {edad} años'
try:
print(validar_edad(25))
print(validar_edad(200))
except EdadInvalidaError as e:
print(f'Error personalizado: {e.mensaje}')
print(f'Edad problemática: {e.edad}')
Edad válida: 25 años Error personalizado: La edad 200 no es válida Edad problemática: 200
class BancoError(Exception):
'''Excepción base para errores bancarios'''
pass
class SaldoInsuficienteError(BancoError):
'''Error cuando no hay suficiente saldo'''
def __init__(self, saldo_actual, monto_solicitado):
self.saldo_actual = saldo_actual
self.monto_solicitado = monto_solicitado
super().__init__(f'Saldo insuficiente: tienes ${saldo_actual}, necesitas ${monto_solicitado}')
class CuentaBloqueadaError(BancoError):
'''Error cuando la cuenta está bloqueada'''
pass
def retirar_dinero(saldo, monto, cuenta_activa=True):
if not cuenta_activa:
raise CuentaBloqueadaError('La cuenta está bloqueada')
if monto > saldo:
raise SaldoInsuficienteError(saldo, monto)
return saldo - monto
try:
nuevo_saldo = retirar_dinero(100, 150)
except SaldoInsuficienteError as e:
print(f'Error de saldo: {e}')
except BancoError as e:
print(f'Error bancario: {e}')
Error de saldo: Saldo insuficiente: tienes $100, necesitas $150
⚡ Casos de Uso Profesionales y Mejores Prácticas
En aplicaciones profesionales, el manejo de excepciones debe ser estratégico y específico. Es importante capturar solo las excepciones que podemos manejar apropiadamente, no usar except genérico sin tipo específico, y siempre limpiar recursos en finally o usar context managers. El logging de errores es crucial para debugging en producción. También debemos considerar la propagación de excepciones: a veces es mejor dejar que la excepción se propague hacia arriba en lugar de manejarla localmente. El principio 'fail fast' sugiere detectar errores lo antes posible en lugar de permitir estados corruptos.
import logging
from contextlib import contextmanager
# Configuración básica de logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class DatabaseError(Exception):
pass
@contextmanager
def conexion_database():
'''Simula una conexión a base de datos'''
logger.info('Estableciendo conexión a la base de datos')
try:
# Simular conexión
conexion = {'conectado': True}
yield conexion
except Exception as e:
logger.error(f'Error en operación de base de datos: {e}')
raise DatabaseError(f'Falla en base de datos: {e}')
finally:
logger.info('Cerrando conexión a la base de datos')
def operacion_critica():
try:
with conexion_database() as db:
if not db['conectado']:
raise DatabaseError('Conexión fallida')
logger.info('Operación completada exitosamente')
return 'Datos procesados'
except DatabaseError as e:
logger.error(f'Error crítico: {e}')
return None
resultado = operacion_critica()
print(f'Resultado: {resultado}')
INFO:__main__:Estableciendo conexión a la base de datos INFO:__main__:Operación completada exitosamente INFO:__main__:Cerrando conexión a la base de datos Resultado: Datos procesados
Ejercicios Prácticos
Validador de Entrada de Usuario
INTERMEDIODescripción:
Crea una función que valide la entrada del usuario para un formulario de registro. Debe manejar diferentes tipos de errores: email inválido, contraseña débil, edad fuera de rango. Crea excepciones personalizadas para cada tipo de error y un sistema de validación completo.
Gestor de Archivos Robusto
AVANZADODescripción:
Implementa una clase que maneje operaciones de archivos (leer, escribir, copiar) con manejo completo de excepciones. Debe manejar archivos no encontrados, permisos insuficientes, espacio en disco, etc. Incluye logging y limpieza automática de recursos.
Calculadora Científica con Manejo de Errores
BÁSICODescripción:
Desarrolla una calculadora que maneje operaciones matemáticas complejas con manejo robusto de errores: división por cero, raíces de números negativos, logaritmos de números no positivos, etc. Crea excepciones específicas para cada tipo de error matemático.
Sistema de Reintentos Automáticos
AVANZADODescripción:
Implementa un decorador que automáticamente reintente una función cuando falle, con backoff exponencial. Debe manejar diferentes tipos de excepciones de manera distinta (algunas se reintentan, otras no) y tener un límite máximo de intentos.
Resumen y Próximos Pasos
El manejo de errores y excepciones es fundamental para crear aplicaciones robustas y profesionales. Hemos cubierto desde conceptos básicos de try-except hasta la creación de excepciones personalizadas y mejores prácticas profesionales. El dominio de estos conceptos permite desarrollar software que puede recuperarse elegantemente de errores y proporcionar mejor experiencia de usuario. En el siguiente tema exploraremos el trabajo con archivos y serialización de datos, donde aplicaremos estos conceptos de manejo de errores en operaciones de entrada/salida que son particularmente propensas a fallos.