Manejo de Archivos y Excepciones

En esta sección aprenderemos sobre el manejo de archivos y excepciones en Python, dos conceptos fundamentales para crear programas robustos y trabajar con datos externos.

Manejo de Archivos

Python proporciona funciones integradas para trabajar con archivos. Los archivos se pueden abrir para lectura, escritura o ambos.

Apertura de Archivos

La función open() se utiliza para abrir archivos. Su sintaxis básica es:

file = open(nombre_archivo, modo)

Modos de Apertura

Existen dos categorías principales de modos: texto y binario.

Modos de Texto

Los archivos de texto se manejan como cadenas de caracteres (strings):

  • 'r' - Lectura (modo por defecto)

  • 'w' - Escritura (crea nuevo archivo o sobrescribe si existe)

  • 'a' - Append (añade al final del archivo)

  • 'r+' - Lectura y escritura

Modos Binarios

Para archivos binarios (imágenes, PDF, etc.), se añade “b” al modo:

  • 'rb' - Lectura binaria

  • 'wb' - Escritura binaria

  • 'ab' - Append binario

  • 'r+b' - Lectura y escritura binaria

Diferencias entre Modo Texto y Binario

  1. Modo Texto:

    • Los datos se manejan como strings

    • Realiza conversiones de caracteres especiales (\n, \r\n)

    • Codificación automática (UTF-8 por defecto)

    • Ideal para archivos .txt, .csv, .py, etc.

# Ejemplo modo texto
with open('archivo.txt', 'r') as f:
    texto = f.read()  # Lee como string
  1. Modo Binario:

    • Los datos se manejan como bytes

    • No realiza conversiones de caracteres

    • No aplica codificación

    • Necesario para archivos no textuales

# Ejemplo modo binario
with open('imagen.jpg', 'rb') as f:
    datos = f.read()  # Lee como bytes

Ejemplos Prácticos

# Copiando una imagen (modo binario)
with open('original.jpg', 'rb') as origen:
    with open('copia.jpg', 'wb') as destino:
        destino.write(origen.read())

# Trabajando con texto (modo texto)
with open('notas.txt', 'w') as f:
    f.write('Hola\nMundo')  # \n se convierte automáticamente al separador de línea del sistema

# Trabajando con CSV (modo texto)
with open('datos.csv', 'r') as f:
    for linea in f:
        campos = linea.strip().split(',')

Lectura de Archivos

Python ofrece varios métodos para leer archivos, cada uno con sus propias características y casos de uso específicos.

Método read()

El método read() lee todo el contenido del archivo como una única cadena de texto:

# Leer todo el archivo de una vez
with open('archivo.txt', 'r') as f:
    contenido = f.read()
    print(contenido)  # Muestra todo el contenido

Ventajas:

  • Simple y directo

  • Útil para archivos pequeños

Desventajas:

  • Puede consumir mucha memoria con archivos grandes

  • No es eficiente para procesamiento línea por línea

Iteración Línea por Línea

Python permite iterar directamente sobre el archivo, leyendo una línea a la vez:

# Leer línea por línea
with open('archivo.txt', 'r') as f:
    for linea in f:
        # strip() elimina espacios en blanco y saltos de línea
        print(linea.strip())

Ventajas:

  • Eficiente en memoria

  • Ideal para archivos grandes

  • Permite procesar el archivo secuencialmente

Desventajas:

  • No permite acceso aleatorio a las líneas

Método readlines()

El método readlines() lee todas las líneas del archivo y las devuelve como una lista:

# Leer todas las líneas en una lista
with open('archivo.txt', 'r') as f:
    lineas = f.readlines()
    # Cada elemento de la lista es una línea del archivo
    for linea in lineas:
        print(linea.strip())

Ventajas:

  • Permite acceso aleatorio a las líneas

  • Útil cuando necesitas manipular las líneas en cualquier orden

  • Facilita operaciones como ordenamiento o filtrado

Desventajas:

  • Consume más memoria que la lectura línea por línea

  • No recomendado para archivos muy grandes

Ejemplo Práctico Comparativo

# Ejemplo que muestra diferentes formas de leer un archivo
def leer_archivo_diferentes_formas(nombre_archivo):
    # Método 1: read()
    print("Usando read():")
    with open(nombre_archivo, 'r') as f:
        contenido = f.read()
        print(f"Contenido completo: {contenido}\n")

    # Método 2: iteración línea por línea
    print("Usando iteración línea por línea:")
    with open(nombre_archivo, 'r') as f:
        for i, linea in enumerate(f, 1):
            print(f"Línea {i}: {linea.strip()}")
    print()

    # Método 3: readlines()
    print("Usando readlines():")
    with open(nombre_archivo, 'r') as f:
        lineas = f.readlines()
        print(f"Número total de líneas: {len(lineas)}")
        for i, linea in enumerate(lineas, 1):
            print(f"Línea {i}: {linea.strip()}")

Consideraciones Adicionales

  1. Manejo de Archivos Grandes:

    • Para archivos grandes, usar iteración línea por línea

    • Evitar read() y readlines() con archivos muy grandes

  2. Codificación:

    • Especificar la codificación al abrir el archivo si es necesario:

    with open('archivo.txt', 'r', encoding='utf-8') as f:
        contenido = f.read()
    
  3. Memoria:

    • read() y readlines() cargan todo el contenido en memoria

    • La iteración línea por línea es más eficiente en memoria

  4. Rendimiento:

    • La iteración línea por línea es generalmente más rápida para procesamiento secuencial

    • readlines() es mejor cuando necesitas acceso aleatorio a las líneas

Escritura en Archivos

Python proporciona varios métodos para escribir en archivos. Es importante entender las diferentes opciones y sus implicaciones.

Método write()

El método write() permite escribir una cadena de texto en el archivo:

# Escribir una cadena simple
with open('archivo.txt', 'w') as f:
    f.write('Hola Mundo\n')  # \n añade un salto de línea

# Escribir múltiples cadenas con write()
with open('archivo.txt', 'w') as f:
    f.write('Primera línea\n')
    f.write('Segunda línea\n')
    f.write('Tercera línea\n')

Características:

  • Escribe exactamente lo que se le pasa

  • No añade automáticamente saltos de línea

  • Retorna el número de caracteres escritos

Método writelines()

El método writelines() permite escribir una secuencia de cadenas:

# Escribir una lista de líneas
lineas = ['Línea 1\n', 'Línea 2\n', 'Línea 3\n']
with open('archivo.txt', 'w') as f:
    f.writelines(lineas)

# Usando comprensión de lista para añadir saltos de línea
lineas = ['Línea 1', 'Línea 2', 'Línea 3']
with open('archivo.txt', 'w') as f:
    f.writelines(linea + '\n' for linea in lineas)

Características:

  • Acepta cualquier iterable de strings

  • No añade separadores automáticamente

  • Más eficiente para escribir múltiples líneas

Modos de Escritura

  1. Modo “w” (write):

    • Crea un nuevo archivo o sobrescribe si existe

    • Borra todo el contenido previo

with open('archivo.txt', 'w') as f:
    f.write('Nuevo contenido')  # Sobrescribe todo el archivo
  1. Modo “a” (append):

    • Añade contenido al final del archivo

    • Mantiene el contenido existente

with open('archivo.txt', 'a') as f:
    f.write('\nContenido adicional')  # Añade al final

Ejemplos Prácticos

  1. Escribir datos estructurados:

# Escribir datos en formato específico
datos = [
    {'nombre': 'Ana', 'edad': 25},
    {'nombre': 'Juan', 'edad': 30}
]

with open('personas.txt', 'w') as f:
    for persona in datos:
        f.write(f"Nombre: {persona['nombre']}, Edad: {persona['edad']}\n")
  1. Combinar write() y writelines():

with open('reporte.txt', 'w') as f:
    # Escribir encabezado
    f.write("REPORTE DIARIO\n")
    f.write("=============\n\n")

    # Escribir contenido
    items = ['Item 1', 'Item 2', 'Item 3']
    f.writelines(f"- {item}\n" for item in items)

Consideraciones Importantes

  1. Manejo de Codificación:

    • Especificar la codificación al abrir el archivo:

    with open('archivo.txt', 'w', encoding='utf-8') as f:
        f.write('Texto con caracteres especiales: áéíóú')
    
  2. Buenas Prácticas:

    • Siempre usar with para garantizar que el archivo se cierre

    • Verificar permisos de escritura

    • Hacer copias de seguridad antes de sobrescribir archivos importantes

  3. Rendimiento:

    • writelines() es más eficiente para múltiples líneas

    • Para archivos grandes, considerar escribir en bloques

    • Evitar abrir y cerrar el archivo repetidamente

  4. Plataformas:

    • Los saltos de línea pueden variar según el sistema operativo

    • Usar os.linesep para compatibilidad multiplataforma

El Administrador de Contexto (with)

En Python, existen dos formas principales de trabajar con archivos: usando el administrador de contexto with o manejando manualmente la apertura y cierre de archivos.

Uso del Administrador de Contexto (with)

El administrador de contexto with se encarga automáticamente de cerrar el archivo cuando terminamos de usarlo:

# Uso recomendado con with
with open('archivo.txt', 'r') as f:
    contenido = f.read()
# Al salir del bloque with, el archivo se cierra automáticamente

Ventajas:

  • Cierre automático del archivo

  • Código más limpio y seguro

  • Manejo automático de recursos

Manejo Manual de Archivos

Sin usar with, debemos abrir y cerrar el archivo manualmente:

# Método no recomendado
f = open('archivo.txt', 'r')
contenido = f.read()
f.close()  # Debemos recordar cerrar el archivo

Desventajas:

  • Requiere cerrar el archivo explícitamente

  • Puede olvidarse cerrar el archivo

  • Más propenso a errores

Ejemplos Comparativos

  1. Lectura Simple:

# Con with (recomendado)
with open('datos.txt', 'r') as archivo:
    datos = archivo.read()
    # El archivo se cerrará automáticamente

# Sin with (no recomendado)
archivo = open('datos.txt', 'r')
datos = archivo.read()
archivo.close()  # No olvidar cerrar
  1. Procesamiento de Múltiples Líneas:

# Con with
with open('numeros.txt', 'r') as archivo:
    for linea in archivo:
        print(int(linea))
# Archivo se cierra automáticamente

# Sin with
archivo = open('numeros.txt', 'r')
for linea in archivo:
    print(int(linea))
archivo.close()  # Debemos recordar cerrar
  1. Escritura de Datos:

# Con with
with open('salida.txt', 'w') as archivo:
    archivo.write('Línea 1\n')
    archivo.write('Línea 2\n')
# Se cierra automáticamente

# Sin with
archivo = open('salida.txt', 'w')
archivo.write('Línea 1\n')
archivo.write('Línea 2\n')
archivo.close()  # No olvidar cerrar

Situaciones donde es Necesario el Manejo Manual

Aunque with es generalmente la mejor opción, hay situaciones donde el manejo manual puede ser necesario:

  1. Mantener un archivo abierto por largo tiempo:

# Archivo de registro que se mantiene abierto
log_file = open('registro.txt', 'a')

def registrar_evento(evento):
    log_file.write(f"{evento}\n")
    log_file.flush()  # Forzar escritura

# Usar el archivo...
registrar_evento("Inicio de programa")
registrar_evento("Operación completada")

# Cerrar al finalizar el programa
log_file.close()
  1. Múltiples operaciones en diferentes partes del código:

archivo = open('datos.txt', 'r')

def procesar_parte1():
    # Leer primeras 10 líneas
    for _ in range(10):
        print(archivo.readline())

def procesar_parte2():
    # Leer las siguientes 5 líneas
    for _ in range(5):
        print(archivo.readline())

procesar_parte1()
procesar_parte2()
archivo.close()

Buenas Prácticas

  1. Usar with siempre que sea posible

  2. Si se maneja el archivo manualmente:

    • Cerrar el archivo tan pronto como sea posible

    • Usar flush() cuando sea necesario forzar la escritura

    • Documentar por qué se eligió el manejo manual

  3. Considerar el uso de with en funciones separadas si se necesita procesar el archivo en partes

Manejo de Excepciones

¿Qué es una Excepción?

Una excepción es un evento que ocurre durante la ejecución de un programa que interrumpe el flujo normal de las instrucciones. En Python, las excepciones son una forma de manejar errores y situaciones inesperadas de manera controlada.

Try-Except Básico

La estructura básica try-except permite ejecutar código que podría generar un error y manejar ese error de forma elegante:

# Ejemplo 1: Conversión de tipos
try:
    numero = int(input("Ingrese un número: "))
    print(f"El número ingresado es: {numero}")
except ValueError:
    print("Error: Debe ingresar un número válido")

# Ejemplo 2: División
try:
    numerador = 10
    denominador = int(input("Ingrese el denominador: "))
    resultado = numerador / denominador
    print(f"El resultado es: {resultado}")
except ZeroDivisionError:
    print("Error: No se puede dividir entre cero")
except ValueError:
    print("Error: Debe ingresar un número válido")

Múltiples Excepciones

Python permite manejar diferentes tipos de excepciones de manera específica:

def leer_numero_desde_archivo(nombre_archivo):
    try:
        with open(nombre_archivo, 'r') as archivo:
            numero = int(archivo.readline())
            resultado = 100 / numero
            return resultado
    except FileNotFoundError:
        print(f"Error: No se encontró el archivo '{nombre_archivo}'")
    except ValueError:
        print("Error: El contenido del archivo no es un número")
    except ZeroDivisionError:
        print("Error: El número leído es cero y no se puede dividir")
    except Exception as e:
        print(f"Error inesperado: {str(e)}")
    return None

Try-Except-Else-Finally

La estructura completa de manejo de excepciones incluye cuatro bloques:

  1. try: Código que puede generar una excepción

  2. except: Maneja la excepción si ocurre

  3. else: Se ejecuta si no hay excepciones

  4. finally: Se ejecuta siempre, haya o no excepciones

Estructura Básica:

def procesar_archivo(nombre_archivo):
    try:
        with open(nombre_archivo, 'r') as archivo:
            contenido = archivo.read()
    except FileNotFoundError:
        print("El archivo no existe")
        return None
    else:
        print("Archivo leído exitosamente")
        return contenido
    finally:
        print("Proceso de archivo finalizado")

Orden de Ejecución con Finally

El bloque finally está diseñado para ejecutar código de limpieza y se ejecuta SIEMPRE, incluso si hay un return en los bloques anteriores:

def ejemplo_orden_ejecucion():
    archivo = None
    try:
        archivo = open('datos.txt', 'r')
        datos = archivo.read()
        return datos
    except FileNotFoundError:
        print("No se encontró el archivo")
        return None
    finally:
        if archivo:
            archivo.close()
            print("Archivo cerrado")

# Si el archivo existe:
# 1. Abre el archivo
# 2. Lee los datos
# 3. Ejecuta finally y cierra el archivo
# 4. Retorna los datos

# Si el archivo no existe:
# 1. Captura FileNotFoundError
# 2. Imprime mensaje de error
# 3. Ejecuta finally (no hay archivo que cerrar)
# 4. Retorna None

Uso Típico de Finally

El bloque finally se usa principalmente para tareas de limpieza:

def procesar_datos():
    conexion = None
    try:
        conexion = abrir_conexion()
        return procesar_informacion(conexion)
    except ConexionError:
        print("Error en la conexión")
        return None
    finally:
        if conexion:
            conexion.cerrar()  # Siempre cierra la conexión

Consideraciones Importantes

  1. El bloque finally siempre se ejecuta, incluso si hay returns en try, except o else

  2. Se usa principalmente para limpieza de recursos

  3. No debe contener lógica de negocio compleja

  4. Es ideal para:

    • Cerrar archivos

    • Cerrar conexiones de red

    • Liberar recursos del sistema

    • Registrar fin de operaciones

Excepciones Comunes y Sus Casos de Uso

  1. ValueError Ocurre cuando una operación recibe un argumento con el tipo correcto pero valor inapropiado:

def procesar_edad():
    try:
        edad = int(input("Ingrese su edad: "))
        if edad < 0:
            raise ValueError("La edad no puede ser negativa")
        return edad
    except ValueError as e:
        print(f"Error: {str(e)}")
        return None
  1. TypeError Ocurre cuando se intenta realizar una operación con tipos de datos incompatibles:

def sumar_valores(a, b):
    try:
        resultado = a + b
        return resultado
    except TypeError:
        print(f"Error: No se pueden sumar {type(a)} y {type(b)}")
        return None

# Ejemplos de uso:
print(sumar_valores(5, "3"))  # Error
print(sumar_valores(5, 3))    # 8
  1. FileNotFoundError Ocurre cuando se intenta acceder a un archivo que no existe:

def leer_configuracion(archivo_config):
    try:
        with open(archivo_config, 'r') as f:
            return f.read()
    except FileNotFoundError:
        print(f"El archivo {archivo_config} no existe")
        return None
  1. IndexError Ocurre al intentar acceder a un índice que está fuera de rango:

def obtener_elemento(lista, indice):
    try:
        return lista[indice]
    except IndexError:
        print(f"Error: El índice {indice} está fuera de rango")
        return None
  1. KeyError Ocurre al intentar acceder a una clave que no existe en un diccionario:

def obtener_valor(diccionario, clave):
    try:
        return diccionario[clave]
    except KeyError:
        print(f"Error: La clave '{clave}' no existe")
        return None

Ejemplos Prácticos Integrados

  1. Procesamiento de Archivo con Números

def procesar_archivo_numeros(nombre_archivo):
    total = 0
    numeros_procesados = 0

    try:
        with open(nombre_archivo, 'r') as archivo:
            for numero_linea, linea in enumerate(archivo, 1):
                try:
                    numero = float(linea.strip())
                    total += numero
                    numeros_procesados += 1
                except ValueError:
                    print(f"Advertencia: Línea {numero_linea} no contiene un número válido")

            if numeros_procesados == 0:
                return 0

            return total / numeros_procesados

    except FileNotFoundError:
        print(f"Error: No se encontró el archivo '{nombre_archivo}'")
        return None
    finally:
        print(f"Se procesaron {numeros_procesados} números válidos")
  1. Validación de Datos de Usuario

def validar_usuario(datos_usuario):
    campos_requeridos = ['nombre', 'edad', 'email']

    try:
        # Verificar campos requeridos
        for campo in campos_requeridos:
            if campo not in datos_usuario:
                raise KeyError(f"Falta el campo {campo}")

        # Validar edad
        edad = int(datos_usuario['edad'])
        if edad < 0 or edad > 120:
            raise ValueError("Edad fuera de rango válido")

        # Validar email (ejemplo simple)
        if '@' not in datos_usuario['email']:
            raise ValueError("Email inválido")

        return True

    except KeyError as e:
        print(f"Error de datos: {str(e)}")
    except ValueError as e:
        print(f"Error de validación: {str(e)}")
    except Exception as e:
        print(f"Error inesperado: {str(e)}")

    return False

Mejores Prácticas

  1. Especificidad en las Excepciones

    • Capturar excepciones específicas en lugar de usar except general

    • Ordenar las excepciones de más específicas a más generales

# Mal
try:
    # código
except Exception as e:
    print(e)

# Bien
try:
    # código
except ValueError:
    print("Error de valor")
except TypeError:
    print("Error de tipo")
except Exception as e:
    print(f"Error inesperado: {e}")
  1. Uso Apropiado de Finally

    • Usar finally para limpieza de recursos

    • No colocar lógica compleja en finally

    • Recordar que finally se ejecuta antes de cualquier return

  2. Manejo de Recursos

    • Preferir el uso de “with” para recursos como archivos

    • Asegurar que los recursos se liberen adecuadamente

  3. Mensajes de Error

    • Proporcionar mensajes claros y útiles

    • Incluir información relevante para la depuración

  4. Alcance del Try

    • Minimizar el código dentro del bloque try

    • Incluir solo el código que puede generar la excepción específica

  5. Documentación

    • Documentar las excepciones que puede lanzar una función

    • Explicar el significado y manejo de cada excepción

Lanzamiento de Excepciones con Raise

Python permite lanzar excepciones de forma explícita usando el comando raise. Esto es útil cuando queremos indicar que ha ocurrido un error en nuestro código.

Uso Básico de Raise

El comando raise permite lanzar cualquiera de las excepciones incorporadas de Python:

def dividir(a, b):
    if b == 0:
        raise ValueError("No se puede dividir entre cero")
    return a / b

# Uso
try:
    resultado = dividir(10, 0)
except ValueError as error:
    print(f"Error: {error}")

Casos Comunes de Uso

  1. Validación de Datos:

def validar_edad(edad):
    if edad < 0:
        raise ValueError("La edad no puede ser negativa")
    if edad > 120:
        raise ValueError("Edad no válida")
    return True

# Uso
try:
    validar_edad(-5)
except ValueError as error:
    print(f"Error: {error}")
  1. Validación de Parámetros:

def procesar_lista(lista):
    if not lista:
        raise ValueError("La lista no puede estar vacía")
    if len(lista) > 100:
        raise ValueError("La lista es demasiado larga")
    return sum(lista)
  1. Manejo de Estados Inválidos:

def actualizar_saldo(saldo, monto):
    if saldo < 0:
        raise ValueError("El saldo no puede ser negativo")
    if monto < 0:
        raise ValueError("El monto no puede ser negativo")
    return saldo + monto

Relanzamiento de Excepciones

A veces queremos capturar una excepción, hacer algo y luego relanzarla:

def procesar_dato(valor):
    try:
        numero = int(valor)
        if numero < 0:
            raise ValueError("Número negativo no permitido")
    except ValueError as e:
        print(f"Ocurrió un error al procesar {valor}")
        raise  # Relanza la última excepción

Cuándo Usar Raise

Es apropiado usar raise cuando:

  1. Los datos o parámetros no cumplen con los requisitos esperados

  2. El programa está en un estado inválido

  3. No se pueden cumplir las precondiciones de una operación

  4. Se detectan valores fuera de rango

Ejemplos Prácticos

  1. Validación de Archivo:

def leer_configuracion(nombre_archivo):
    if not nombre_archivo.endswith('.txt'):
        raise ValueError("El archivo debe ser .txt")

    try:
        with open(nombre_archivo, 'r') as archivo:
            return archivo.read()
    except FileNotFoundError:
        raise ValueError(f"No se encontró el archivo {nombre_archivo}")
  1. Validación de Datos de Usuario:

def validar_usuario(nombre, edad):
    if not nombre:
        raise ValueError("El nombre no puede estar vacío")

    try:
        edad = int(edad)
        if edad < 0 or edad > 120:
            raise ValueError("Edad fuera de rango")
    except ValueError:
        raise ValueError("La edad debe ser un número válido")

    return True

Buenas Prácticas

  1. Mensajes Claros:

    • Proporcionar mensajes de error descriptivos

    • Incluir detalles relevantes en el mensaje

  2. Tipo Apropiado:

    • Usar el tipo de excepción más específico posible

    • ValueError para errores de valor

    • TypeError para errores de tipo

    • RuntimeError para errores de ejecución generales

  3. Documentación:

    • Documentar las excepciones que puede lanzar una función

    • Explicar las condiciones que causan cada excepción