5  Manejo de archivos y excepciones

Objetivo

El objetivo de esta clase es proporcionar a los estudiantes una comprensión práctica y aplicada del manejo de excepciones en Python, junto con la manipulación de archivos en diferentes formatos como CSV, TXT y JSON.

Los archivos necesarios para este módulo se encuentran en el repositorio de GitHub

5.1 Excepciones en Python

Las excepciones en Python son una herramienta muy potente que la gran mayoría de lenguajes deprogramación modernos tienen. Se trata de una forma de controlar el comportamiento de un programacuando se produce un error.

Esto es muy importante ya que, salvo que tratemos este error, el programa se parará y esto es algoque en determinadas aplicaciones no es una opción válida.

Imaginemos que tenemos el siguiente código con dos variables a y b yrealizamos su división a/b.

a= 4
b =2
c =a/b

Pero imaginemos ahora que por cualquier motivo las variables tienen otro valor y que, por ejemplo,b tiene el valor 0. Si intentamos hacer la división entre cero, esteprograma dará un error y su ejecución terminará de manera abrupta.

a= 4; b = 0
print/b)

Ese error que decimos que ha ocurrido es lanzado (raise en Inglés) por Python ya que ladivisión entre cero es una operación que matemáticamente no está definida.

Se trata de la excepción ZeroDivisionError. En el siguiente enlace tenemos un listado de todas las excepciones con lasque nos podemos encontrar.

Veamos un ejemplo con otra excepción. ¿Que pasaría si intentásemos sumar un número con un texto?Evidentemente esto no tiene ningún sentido y Python define una excepción para esto llamadaTypeError.

Con base en esto, es muy importante controlar las excepciones, porque por muchas comprobacionesque realicemos es posible que en algún momento ocurra una y, si no se hace nada, el programa separará.

¿Te imaginas que en un avión, un tren o un cajero automático tiene un error que lanza unaexcepción y se detiene por completo?

Una primera aproximación al control de excepciones podría ser el siguiente ejemplo. Podemosrealizar una comprobación manual de que no estamos dividiendo por cero, para así evitar tener unerror tipo ZeroDivisionError.

Sin embargo, es complicado escribir código que contemple y que prevenga todo tipo de excepciones.Para ello, veremos más adelante el uso de except.

a= 5
b =0

ifb!=0:
    print(a/b)
else:
    print('No se puede dividir!')

5.1.1 Uso de raise

También podemos ser nosotros los que lancemos una excepción. Volviendo a los ejemplos usados en el apartado anterior, podemos ser nosotros los que levantemos ZeroDivisionError o TypeError usando raise. La sintaxis es muy fácil.

raise ZeroDivisionError('Información de la excepción')
raise TypeError('Información de la excepción')

Se puede ver como el string que hemos pasado se imprime a continuación de la excepción. Se puede llamar también sin ningún parámetro como se hace a continuación.

raise ZeroDivisionError

Visto esto, ya sabemos como una excepción puede ser lanzada. Existen dos maneras principalmente:

  • Hacemos una operación que no puede ser realizada (como dividir por cero). En este caso Python se encarga de lanzar automáticamente la excepción.
  • Podemos lanzar nosotros una excepción manualmente, usando raise.

A continuación veremos que podemos hacer para controlar estas excepciones, y que hacer cuando se lanza una para que no se interrumpa la ejecución del programa.

5.1.2 Uso de try y except

La buena noticia es que las excepciones que hemos visto antes, pueden ser capturadas y manejadas adecuadamente, sin que el programa se detenga. Veamos un ejemplo con la división entre cero.

a = 5; b = 0
try:
    c = a/b
except ZeroDivisionError:
    print('No se ha podido realizar la división')

En este caso no verificamos que b!=0. Directamente intentamos realizar la división y en el caso de que se lance la excepción ZeroDivisionError, la capturamos y la tratamos adecuadamente.

La diferencia con el ejemplo anterior es que ahora no se para el programa y se puede seguir ejecutando. Entonces, lo que hay dentro del try es la sección del código que podría lanzar la excepción que se está capturando en el except. Por lo tanto cuando ocurra una excepción, se entra en el except pero el programa no se para.

También se puede capturar diferentes excepciones como se ve en el siguiente ejemplo.

try:
    #c = 5/0
    d = 2 + 'Hola'
except ZeroDivisionError:
    print('No se puede dividir entre cero!')
except TypeError:
    print('Problema de tipos!')

Puedes también hacer que un determinado número de excepciones se traten de la misma manera con el mismo bloque de código. Sin embargo suele ser más interesante tratar a diferentes excepciones de diference manera.

try:
    #c = 5/0
    d = 2 + 'Hola'
except (ZeroDivisionError, TypeError):
    print('Error en la ejecución')

Otra forma si no sabes que excepción puede saltar, puedes usar la clase genérica Exception. En este caso se controla cualquier tipo de excepción. De hecho todas las excepciones heredan de Exception (ver documentación).

try:
    #c = 5/0
    d = 2 + 'Hola'
except Exception:
    print('Ha habido una excepción')

No obstante hay una forma de saber que excepción ha sido la que ha ocurrido.

try:
    d = 2 + 'Hola' # Si comentas esto entra en ZeroDivisionError
except Exception as ex:
    print('Ha habido una excepción:', type(ex))

5.1.3 Uso de else

Al ya explicado try y except le podemos añadir un bloque else adicional. Dicho bloque se ejecutará si no ha ocurrido ninguna excepción. Fíjate en la diferencia entre los siguientes códigos.

try:
    x = 2/4
except:
    print('Entra en except, ha ocurrido una excepción')
else:
    print('Entra en else, no ha ocurrido ninguna excepción')

5.1.4 Uso de finally

A los ya vistos bloques try, except y else podemos añadir un bloque más, el finally. Dicho bloque se ejecuta siempre, haya o no haya habido excepción.

Este bloque se suele usar si queremos ejecutar algún tipo de acción de limpieza. Si por ejemplo estamos escribiendo datos en un fichero pero ocurre una excepción, tal vez queramos borrar el contenido que hemos escrito con anterioridad, para no dejar datos inconsistenes en el fichero.

En el siguiente código vemos un ejemplo. Haya o no haya excepción el código que haya dentro de finally será ejecutado.

try:
    x = 2/2
except:
    print('Entra en except, ha ocurrido una excepción')
else:
    print('Entra en else, ha ocurrido una excepción')
finally:
    print('Entra en finally, se ejecuta el bloque finally')

5.1.5 Excepciones personalizadas

A pesar de que Python define un conjunto de excepciones por defecto, podrían no ser suficientes para nuestro programa. En ese caso, deberíamos definir nuestra propia excepción.

Si queremos crear una excepción, solamente tenemos que crear una clase que herede de la clase Exception. Es tan sencillo como el siguiente ejemplo.

class MiExcepcionPersonalizada(Exception):
    pass

Y ya podríamos lanzarla con raise cuando quisiéramos.

raise MiExcepcionPersonalizada('Texto personalizado')

También se pueden pasar parámetros de entrada al lanzarla. Esto es muy útil para dar información adicional a la excepción. En el siguiente ejemplo se pasan dos parámetros. Para ello tenemos que modificar la función __init__() definida por defecto al heredar. Veamos dos formas.

class MiExcepcion(Exception):
    def __init__(self, mensaje, informacion):
        print(mensaje)
        print(informacion)
    
raise MiExcepcion("Mi Mensaje", "Mi Informacion")
class MiExcepcion(Exception):
    def __init__(self, mensaje, informacion):
        self.mensaje = mensaje
        self.informacion = informacion
    
try:
    raise MiExcepcion("Mi Mensaje", "Mi Informacion")
except MiExcepcion as e:
    print(e.mensaje)
    print(e.informacion)

5.1.6 Ejemplos

Un ejemplo muy típico de excepciones es en el manejo de archivos. Se intenta abrir, pero se captura una posible excepción. De hecho si entras en la documentación de open se indica que OSError es lanzada si el archivo no puede ser abierto.

try:
    with open('archivo_1.txt') as file:
        read_data = file.read()
except:
    print('No se pudo abrir')

Como ya hemos comentado, en el except también se puede capturar una excepción concreta. Dependiendo de nuestro programa, tal vez queramos tratar de manera distinta diferentes tipos de excepciones, por lo que es una buena práctica especificar que tipo de excepción que estamos gestionando.

try:
    with open('archivo_1.txt') as file:
        read_data = file.read()
except OSError:
    print('OSError. No se pudo abrir')

En este otro ejemplo vemos el uso de los bloques try, except, else y finally todos juntos.

import csv

try:
    with open('sample_data.csv', mode='r') as file:
        reader = csv.reader(file)
        data = [row for row in reader]
except FileNotFoundError:
    print('El archivo no fue encontrado.')
except PermissionError:
    print('No tienes permiso para leer este archivo.')
except Exception as e:
    print(f'Ocurrió un error inesperado: {e}')
else:
    print('El archivo se leyó correctamente.')
    print('Contenido del archivo:')
    for row in data:
        print(row)
finally:
    print('Proceso de lectura del archivo CSV completado.')

5.2 Lectura de archivos en Python

Al igual que en otros lenguajes de programación, en Python es posible abrir archivos y leer su contenido. Los archivos pueden ser de lo más variado, desde un simple texto a contenido binario.

Imagínate entonces que tienes un archivo de texto con unos datos, como podría ser un .txt o un .csv, y quieres leer su contenido para realizar algún tipo de procesado sobre el mismo.

Podemos abrir el archivo con la función open() pasando como argumento el nombre del archivo que queremos abrir.

data = open('sample_data.txt')

5.2.1 Métodos read y close

Con open() tendremos ya en el contenido del documento listo para usar y podemos imprimir su contenido con read(). El siguiente código imprime todo el contenido del archivo.

print(data.read())

El método close se utiliza para cerrar un archivo que ha sido abierto previamente con la función open(). Cuando trabajas con archivos en Python, es importante cerrarlos después de haber terminado de leer o escribir en ellos. Esto libera recursos y asegura que cualquier operación pendiente se termine correctamente antes de cerrar el archivo.

data.close()

5.2.2 Método readline

Es posible también leer un número de líneas determinado y no todo el archivo de golpe. Para ello hacemos uso del método readline(). Cada vez que se llama a la función, se lee una línea.

data = open('sample_data.txt')
print(data.readline())
print(data.readline())
print(data.readline())
data.close()

5.2.3 Método readlines

Existe otro método llamado readlines(), que devuelve una lista donde cada elemento es una línea del archivo.

data = open('sample_data.txt')
lineas = data.readlines()
print(lineas)
data.close()

5.2.4 Argumentos de open

Hasta ahora hemos visto la función open() con tan sólo un argumento de entrada, el nombre del archivo. Lo cierto es que existe un segundo argumento que es importante especificar. Se trata del modo de apertura del archivo. En la documentación oficial se explica en detalle.

  • 'r': Por defecto, para leer el archivo.
  • 'w': Para escribir en el archivo.
  • 'x': Para la creación del archivo, fallando si ya existe.
  • 'a': Para añadir contenido a un archivo existente.
  • 'b': Para abrir en modo binario.

Por lo tanto lo estrictamete correcto si queremos leer el archivo sería hacer lo siguiente. Aunque el modo 'r' sea por defecto, es una buena práctica indicarlo para darle a entender a otras personas que lean nuestro código que no queremos modificarlo, tan solo leerlo.

data = open('sample_data.txt')
try:
    pass
finally:
    data.close()

Además, existe otra forma de cerrar el archivo automáticamente. Si hacemos uso se with(), el archivo se cerrará automáticamente una vez se salga de ese bloque de código.

with open('sample_data.txt') as data:
    for linea in data.readlines():
        print(linea, end='')

5.3 Escritura de archivos en Python

Lo primero que debemos de hacer para escribir en un archivo es crear un objeto para el este, con el nombre que queramos. Por lo tanto, con la siguiente línea estamos creando un archivo con el nombre datos_guardados.txt y, en caso de que exista, borra el contenido del mismo.

file = open('datos_guardados.txt', 'w')
file.close()

Si por el contrario queremos añadir el contenido al ya existente en un archivo de antes, podemos hacerlo en el modo append como hemos indicado.

5.3.1 Método write

Veamos ahora como podemos añadir contenido. Empecemos escribiendo un texto.

file = open('datos_guardados.txt', 'w')
file.write('Contenido a escribir')
file.close()

Por lo tanto si ahora abrimos el archivo datos_guardados.txt, veremos como efectivamente contiene una línea con Contenido a escribir.

Es muy importante el uso de close(), ya que si dejamos el archivo abierto, podríamos llegar a tener un comportamiento inesperado que queremos evitar. Por lo tanto, siempre que se abre un archivo es necesario cerrarlo cuando hayamos acabado. Recuerda que usando el entorno with, se hace esta operación automáticamente.

Compliquemos un poco más las cosas. Ahora vamos a guardar una lista de elementos en el archivo, donde cada elemento de la lista se almacenará en una línea distinta.

file = open("datos_guardados.txt", 'w')

lista = ['Manzana', 'Pera', 'Banano']

for linea in lista:
    file.write(linea + "\n")

file.close()

Es importante añadir el salto de línea porque, por defecto, no se añade. Además, si queremos que cada elemento de la lista se almacena en una línea distinta, será necesario su uso.

5.3.2 Método writelines

También podemos usar el método writelines() y pasarle una lista. Dicho método se encargará de guardar todos los elementos de la lista en el archivo.

file = open('datos_guardados.txt', 'w')
lista = ['Manzana', 'Pera', 'Banano']

file.writelines(lista)
file.close()

Al abrir el arcvhivo, nos daremos cuenta de que en realidad lo que se guarda es ManzanaPeraBanano, todo junto. Si queremos que cada elemento se almacene en una línea distinta, deberíamos añadir el salto de línea en cada elemento de la lista como se muestra a continuación.

file = open('datos_guardados.txt', 'w')
lista = ['Manzana\n', 'Pera\n', 'Banano\n']

file.writelines(lista)
file.close()

5.4 Manejo de archivos .json con Python

El formato JSON fue inspirado por la sintaxis de JavaScript (un lenguaje de programación usado para desarrollo web). Pero desde entonces se ha convertido en un formato de datos independiente del lenguaje de programación. JSON es un formato usado para almacenar o representar datos.

5.4.1 Estructura JSON

Veamos su estructura básica con un ejemplo que representa una orden de pizza:

{ 
    "tamano": "mediana",
    "precio": 15.67,
    "toppings": ["champinones", "pepperoni", "albahaca"],
    "queso_extra": false,
    "delivery": true,
    "cliente": {
        "nombre": "Jane Doe",
        "telefono": null,
        "correo": "janedoe@email.com"
    }
}

Como podemos observar, la estructura se conforma mediante pares de claves-valores, parecido a un diccionario de Python. Cada clave tiene la posibilidad de almacenar deferentes tipos de datos: ya sean cadenas, enteros, flotantes, booleanos, listas, etc. Y en caso de que la estructura posea dos o más pares clave-valor estos deberán encontrarse separados mediante una coma (,).

En la página oficial de JSON y en la guía de estilos de Google se puede encontrar información adicional.

5.4.2 Módulo json

En Python existe el módulo json que nos permite convertir objetos JSON a diccionarios de una forma muy sencilla. Veamos un par de ejemplos.

Comencemos con algo sencillo. Por ejemplo, generemos un diccionario utilizando un string con formato JSON. Para esto haremos uso de la función loads.

import json

json_object =  '''{
    "nombre": "Eduardo",
    "edad": 27,
    "correo": "eduardo78d@gmail.com",
    "cursos": ["Python", "MongoDB", "Flask"]
}'''
print(json_object)
print(type(json_object))
print('\n=================================\n')
user = json.loads(json_object)
print(user)
print(type(user))

Ahora, si quisiéramos hacer lo inverso, usaremos la función dumps, que nos permite serializar un objeto Python a un objeto JSON.

import json

user = {
    "nombre": "Eduardo",
    "edad": 27,
    "correo": "eduardo78d@gmail.com",
    "cursos": ["Python", "MongoDB", "Flask"]
}
print(user)
print(type(user))
print('\n=================================\n')

json_object = json.dumps(user)
print(json_object)
print(type(json_object))

5.4.3 Archivos .json

Ya sabemos tanto crear diccionarios a partir de objetos JSON, como crear objetos JSON a partir de diccionarios. Ahora aprenderemos tanto leer como crear archivos .json.

Que te parece si comenzamos con la lectura. Para esto, el código pudiera quedar de la siguiente manera:

import json

with open('sample_data.json') as f:
    instance = json.load(f)
    
    for movie in instance['movies']:
        print(f'* {movie}')

Ahora, hagamos lo inverso. Creemos un archivo .json a partir de un objeto Python. Para ello me apoyaré del siguiente listado de usuarios.

users = [
      {
        "nombre": "Eduardo",
        "edad": 27
      },
      {
        "nombre": "Raquel",
        "edad": 29
      },
      {
        "nombre": "Fernando",
        "edad": 25
      },
      {
        "nombre": "Guadalupe",
        "edad": 30
      }
]

Nuestro código debería quedar quedar de la siguiente forma:

import json

with open('users.json', 'w') as f:
  json.dump(users, f, indent=4)

Lo interesante de la función dumps es que, en caso deseemos serializar el objeto utilizando un orden alfabético en las claves, podemos hacer uso del parámetro sort_keys.

import json

usuario = {
    'username': 'eduardo_gpg',
    'nombre': 'Eduardo Ismael',
    'edad': 27,
    'correo': 'eduardo78d@gmail.com'
}

data = json.dumps(usuario, indent=4, sort_keys=True)
print(data)

5.5 Ejercicios prácticos

  1. Cree un nuevo Notebook.
  2. Guarde el archivo como Ejercicios_practicos_clase_5.ipynb.
  3. Asigne un título H1 con su nombre.

5.5.1 Ejercicio práctico 1

Escribe un programa en Python que lea un archivo CSV llamado estudiantes.csv y guarde la información de cada estudiante en un archivo de texto resultados.txt. El archivo CSV contiene tres columnas: Nombre, Edad y Calificación. Si el archivo estudiantes.csv no existe, el programa debe manejar esta excepción adecuadamente y mostrar un mensaje al usuario. Si la calificación de un estudiante es menor a 0 o mayor a 5, el programa debe levantar una excepción personalizada llamada CalificacionInvalidaError e incluir un manejo adecuado de la excepción, avisando al usaurio y cambiando la nota por NaN.

  1. Crea el archivo estudiantes.csv con datos ficticios.
  2. Lee el archivo CSV y extrae la información. No use el entorno with.
  3. Escribe los datos en el archivo de texto resultados.txt en el siguiente formato: Nombre: <nombre>, Edad: <edad>, Calificación: <calificacion>.
  4. Implementa un bloque try-except-else-finally para manejar los posibles errores:
    • FileNotFoundError si el archivo estudiantes.csv no existe.
    • CalificacionInvalidaError si una calificación es inválida.
  5. El bloque finally debe cerrar cualquier archivo abierto.

5.5.2 Ejercicio práctico 2

Escribe un programa en Python que lea el archivo JSON sample_data.json usado en clase y luego los guarde en otro archivo llamado sample_data_cipher.json, en el que los valores de cada clave en el archivo deben estar cifrados usando el cifrado César con desplazamiento de 3 posiciones en el alfabeto, aplique igual procedimiento a cada dígito de los números. El nombre de las claves no debe cifrarse. Por ejemplo, si en el archivo JSON de entrada existe la entrada

    "Title" : "The Shawshank Redemption",
    "US Gross" : 28241469

en el archivo de salida debe estar:

    "Title" : "Wkh Vkdzvkdqn Uhghpswlrq",
    "US Gross" : 51574792

Si el archivo JSON no existe o tiene un formato incorrecto, el programa debe manejar estas excepciones.