Clase 5: Persistencia de datos y administración de problemas

Escritura y lectura a archivos

Nuestros programas necesitan interactuar con el mundo exterior. Hasta ahora utilizamos la función print() para imprimir por pantalla mensajes y resultados. Para leer o escribir un archivo primero debemos abrirlo, utilizando la función open()

f = open('../data/names.txt')   # Abrimos el archivo (para leer)
f
<_io.TextIOWrapper name='../data/names.txt' mode='r' encoding='UTF-8'>
s = f.read()                    # Leemos el archivo
f.close()                       # Cerramos el archivo
print(s[:100])
Aaa
Aaron
Aba
Ababa
Ada
Ada
Adam
Adlai
Adrian
Adrienne
Agatha
Agnetha
Ahmed
Ahmet
Aimee
Al
Ala
Alain

Esta secuencia básica de trabajo en adecuada y muy común en el trabajo con archivos. Sin embargo, hay un potencial problema, que ocurrirá si hay algún error entre la apertura y el cierre del archivo. Para ello existe una sintaxis alternativa

with open('../data/names.txt') as fi:
  s = fi.read()
print(s[:50])
Aaa
Aaron
Aba
Ababa
Ada
Ada
Adam
Adlai
Adrian
Adri
# fi todavía existe pero está cerrado
fi
<_io.TextIOWrapper name='../data/names.txt' mode='r' encoding='UTF-8'>
type(fi)
_io.TextIOWrapper

La palabra with es una palabra reservada del lenguaje y la construcción se conoce como contexto. Básicamente dice que todo lo que está dentro del bloque se realizará en el contexto en que f es el objeto de archivo abierto para lectura.

Ejemplos

Vamos a repasar algunos de los conceptos discutidos las clases anteriores e introducir algunas nuevas funcionalidades con ejemplos

Ejemplo 05-1

fname = '../data/names.txt'
n = 0                           # contador
minlen = 3                      # longitud mínima
maxlen = 4                      # longitud máxima

with  open(fname, 'r') as fi:
  lines = fi.readlines()        # El resultado es una lista

for line in lines:
  if minlen <= len(line.strip()) <= maxlen:
    n += 1
    print(line.strip(), end=', ')  # No Newline

print('\n')
if minlen == maxlen:
  mensaje = f"Encontramos {n} palabras que tienen {minlen} letras"
else:
  mensaje = f"Encontramos {n} palabras que tienen entre {minlen} y {maxlen} letras"

print(mensaje)
Aaa, Aba, Ada, Ada, Adam, Ala, Alan, Alex, Alf, Ama, Ami, Amir, Amos, Amy, Ana, Andy, Ann, Anna, Anna, Anne, Anya, Arne, Art, Axel, Bart, Bea, Ben, Bert, Beth, Bib, Bill, Bob, Bob, Boob, Boyd, Brad, Bret, Bub, Buck, Bud, Carl, Cary, Case, Cdc, Chet, Chip, Clay, Clem, Cody, Cole, Cory, Cris, Curt, Dad, Dale, Dan, Dana, Dani, Dave, Dawn, Dean, Deb, Debi, Deed, Del, Dick, Did, Dion, Dirk, Dod, Don, Donn, Dora, Dori, Dory, Doug, Drew, Dud, Duke, Earl, Eddy, Eke, Eli, Elsa, Emil, Emma, Enya, Ere, Eric, Erik, Esme, Eva, Evan, Eve, Eve, Ewe, Eye, Fay, Fred, Gag, Gaia, Gail, Gale, Gary, Gay, Gene, Gig, Gigi, Gil, Gill, Glen, Gog, Greg, Guy, Hal, Hank, Hans, Harv, Hein, Herb, Hohn, Hon, Hope, Hsi, Huey, Hugh, Huh, Hui, Hume, Hurf, Hwa, Iain, Ian, Igor, Iii, Ilya, Ima, Imad, Ira, Isis, Izzy, Jack, Jade, Jan, Jane, Jarl, Jay, Jean, Jef, Jeff, Jem, Jen, Jenn, Jess, Jill, Jim, Jin, Jiri, Joan, Job, Jock, Joe, Joel, John, Jon, Jong, Joni, Joon, Jos, Jose, Josh, Juan, Judy, Juha, Jun, June, Juri, Kaj, Kari, Karl, Kate, Kay, Kee, Kees, Ken, Kenn, Kent, Kiki, Kim, King, Kirk, Kit, Knut, Kory, Kris, Kurt, Kyle, Kylo, Kyu, Lana, Lar, Lara, Lars, Lea, Leah, Lee, Leif, Len, Leo, Leon, Les, Lex, Liam, Lila, Lin, Lisa, List, Liz, Liza, Lois, Lola, Lord, Lori, Lou, Loyd, Luc, Lucy, Lui, Luis, Luke, Lum, Lynn, Mac, Mah, Mann, Mara, Marc, Mark, Mary, Mat, Mats, Matt, Max, May, Mayo, Meg, Mick, Miek, Mike, Miki, Milo, Moe, Mott, Mum, Mwa, Naim, Nan, Nate, Neal, Ned, Neil, Nhan, Nici, Nick, Nils, Ning, Noam, Noel, Non, Noon, Nora, Norm, Nou, Novo, Nun, Ofer, Olaf, Old, Ole, Oleg, Olof, Omar, Otto, Owen, Ozan, Page, Pam, Pap, Part, Pat, Paul, Pdp, Peep, Pep, Per, Pete, Petr, Phil, Pia, Piet, Pim, Ping, Pip, Poop, Pop, Pria, Pup, Raif, Raj, Raja, Ralf, Ram, Rand, Raul, Ravi, Ray, Real, Rees, Reg, Reid, Rene, Renu, Rex, Ric, Rich, Rick, Rik, Rob, Rod, Rolf, Ron, Root, Rose, Ross, Roy, Rudy, Russ, Ruth, S's, Saad, Sal, Sam, Sara, Saul, Scot, Sean, Sees, Seth, Shai, Shaw, Shel, Sho, Sid, Sir, Sis, Skef, Skip, Son, Spy, Sri, Ssi, Stan, Stu, Sue, Sus, Suu, Syd, Syed, Syun, Tad, Tai, Tait, Tal, Tao, Tara, Tat, Ted, Teet, Teri, Tex, Thad, The, Theo, Tim, Timo, Tip, Tit, Tnt, Toby, Todd, Toft, Tom, Tony, Toot, Tor, Tot, Tran, Trey, Troy, Tuan, Tuna, Uma, Una, Uri, Urs, Val, Van, Vern, Vic, Vice, Vick, Wade, Walt, Wes, Will, Win, Wolf, Wow, Zoe, Zon,

Encontramos 420 palabras que tienen entre 3 y 4 letras

Hemos utilizado aquí:

  • Apertura, lectura, y cerrado de archivos

  • Iteración en un loop for

  • Bloques condicionales (if/else)

  • Formato de cadenas de caracteres con reemplazo

  • Impresión por pantalla

La apertura de archivos se realiza utilizando la función open (este es un buen momento para mirar su documentación) con dos argumentos: el primero es el nombre del archivo y el segundo el modo en que queremos abrirlo (en este caso la r indica lectura).

Con el archivo abierto, en la línea 9 leemos línea por línea todo el archivo. El resultado es una lista, donde cada elemento es una línea.

Recorremos la lista, y en cada elemento comparamos la longitud de la línea con ciertos valores. Imprimimos las líneas seleccionadas

Finalmente, escribimos el número total de líneas.

Veamos una leve modificación de este programa

Ejemplo 05-2

lines[0]
'Aaan'
"""Programa para contar e imprimir las palabras de una longitud dada"""

fname = '../data/names.txt'

n = 0                           # contador
minlen = 3                      # longitud mínima
maxlen = 4                      # longitud máxima

with  open(fname, 'r') as fi:
  for line in fi:
    p = line.strip().lower()
    if (minlen <= len(p) <= maxlen) and (p == p[::-1]):
      n += 1
      print('({:02d}): {}'.format(n, p), end=', ')  # Vamos numerando las coincidencias
print('\n')
if minlen == maxlen:
  mensaje = f"Encontramos un total de {n} palabras capicúa que tienen {minlen} letras"
else:
  mensaje = f"Encontramos un total de {n} palabras capicúa que tienen entre {minlen} y {maxlen} letras"

print(mensaje)
(01): aaa, (02): aba, (03): ada, (04): ada, (05): ala, (06): ama, (07): ana, (08): anna, (09): anna, (10): bib, (11): bob, (12): bob, (13): boob, (14): bub, (15): cdc, (16): dad, (17): deed, (18): did, (19): dod, (20): dud, (21): eke, (22): ere, (23): eve, (24): eve, (25): ewe, (26): eye, (27): gag, (28): gig, (29): gog, (30): huh, (31): iii, (32): mum, (33): nan, (34): non, (35): noon, (36): nun, (37): otto, (38): pap, (39): pdp, (40): peep, (41): pep, (42): pip, (43): poop, (44): pop, (45): pup, (46): s's, (47): sees, (48): sis, (49): sus, (50): tat, (51): teet, (52): tit, (53): tnt, (54): toot, (55): tot, (56): wow,

Encontramos un total de 56 palabras capicúa que tienen entre 3 y 4 letras

Aquí en lugar de leer todas las líneas e iterar sobre las líneas resultantes, iteramos directamente sobre el archivo abierto.

Además incluimos un string al principio del archivo, que servirá de documentación, y puede accederse mediante los mecanismos usuales de ayuda de Python.

Imprimimos el número de palabra junto con la palabra, usamos 02d, indicando que es un entero (d), que queremos que el campo sea de un mínimo número de caracteres de ancho (en este caso 2). Al escribirlo como 02 le pedimos que complete los vacíos con ceros.

"""Programa para contar e imprimir las palabras de una longitud dada"""

fname = '../data/names.txt'

n = 0                           # contador
minlen = 3                      # longitud mínima
maxlen = 4                      # longitud máxima
L = []
with  open(fname, 'r') as fi:
  for line in fi:
    p = line.strip().lower()
    if (minlen <= len(p) <= maxlen) and (p == p[::-1]):
      n += 1
      #ss += f"\n{p}"  # ss += "\n" + p
      L.append(p)  # L += [p]
ss = " ".join(L)
if minlen == maxlen:
  mensaje = f"Encontramos un total de {n} palabras capicúa que tienen {minlen} letras"
else:
  mensaje = f"Encontramos un total de {n} palabras capicúa que tienen entre {minlen} y {maxlen} letras"

print(mensaje)
with open('../data/tmp.txt','w') as fo:
    fo.write(ss)
Encontramos un total de 56 palabras capicúa que tienen entre 3 y 4 letras

Archivos comprimidos

Existen varias formas de reducir el tamaño de los archivos de datos. Varios factores, tales como el sistema operativo, nuestra familiaridad con cada uno de ellos, le da una cierta preferencia a algunos de los métodos disponibles. Veamos cómo hacer para leer y escribir algunos de los siguientes formatos: zip, gzip, bz2

import gzip
import bz2
with gzip.open('../data/palabras.words.gz', 'rb') as fi:
  a = fi.read()
with gzip.open('../data/palabras.words.gz', 'r') as fi:
  b = fi.read()
b[:30]
b'xc3x81fricanxc3x81ngelanxc3xa1baconxc3xa1bsida'
l= a.splitlines()
print(l[:10])
[b'xc3x81frica', b'xc3x81ngela', b'xc3xa1baco', b'xc3xa1bsida', b'xc3xa1bside', b'xc3xa1cana', b'xc3xa1caro', b'xc3xa1cates', b'xc3xa1cido', b'xc3xa1cigos']
a[:30]
b'xc3x81fricanxc3x81ngelanxc3xa1baconxc3xa1bsida'
str(l[0])
"b'\xc3\x81frica'"
type(l[0])
bytes

Nota

Vemos que el archivo tiene algunos caracteres que no podemos interpretar. Por ejemplo:

l[0] = "b'\\xc3\\x81frica'"
l[0] = str(l[0])

Esto indica que la variable es del tipo “bytes”, que es la manera en que python describe los strings, pero hay un caracter que no sabemos como mostrar. Para hacerlo debemos codificarlo:

str(l[0], encoding='utf-8') -> 'África'
str(l[0], encoding='utf-8')
'África'

Con todo esto podríamos escribir (si tuviéramos necesidad) una función que puede leer un archivo en cualquiera de estos formatos

import gzip
import bz2
from os.path import splitext
import zipfile

def abrir(fname, modo='r'):
  if fname.endswith('gz'):
    fi= gzip.open(fname, mode=modo)
  elif fname.endswith('bz2'):
    fi= bz2.open(fname, mode=modo)
  elif fname.endswith('zip'):
    fi= zipfile.ZipFile(fname, mode=modo)
  else:
    fi = open(fname, mode=modo)
  return fi
ff= abrir('../data/palabras.words.gz')
a = ff.read()
ff.close()
l = a.splitlines()
print(str(l[0], encoding='utf-8'))
África

Ejercicios 05 (a)

  1. Realice un programa que:

  • Lea el archivo names.txt

  • Guarde en un nuevo archivo (llamado “pares.txt”) palabra por medio del archivo original (la primera, tercera, …) una por línea, pero en el orden inverso al leído

  • Agregue al final de dicho archivo, las palabras pares pero separadas por un punto y coma (;)

  • En un archivo llamado “longitudes.txt” guarde las palabras ordenadas por su longitud, y para cada longitud ordenadas alfabéticamente.

  • En un archivo llamado “letras.txt” guarde sólo aquellas palabras que contienen las letras w,x,y,z, con el formato:

    • w: Walter, ….

    • x: Xilofón, …

    • y: ….

    • z: ….

  • Cree un diccionario, donde cada key es la primera letra y cada valor es una lista, cuyo elemento es una tuple (palabra, longitud). Por ejemplo:

d['a'] = [('Aaa',3),('Anna', 4), ...]
  1. Realice un programa para:

    • Leer los datos del archivo aluminio.dat y poner los datos del elemento en un diccionario de la forma:

    d = {'S': 'Al', 'Z':13, 'A':27, 'M': '26.98153863(12)', 'P': 1.0000, 'MS':'26.9815386(8)'}
    
    • Modifique el programa anterior para que las masas sean números (float) y descarte el valor de la incerteza (el número entre paréntesis)

    • Agregue el código necesario para obtener una impresión de la forma:

    Elemento: Al
    Número Atómico: 13
    Número de Masa: 27
    Masa: 26.98154
    

Note que la masa sólo debe contener 5 números decimales


Nota

Los archivos de texto “names.txt” y “aluminio.txt” (así como otros archivos usados en las clases) pueden encontrarse en la carpeta intro-python

Atrapar y administrar errores

Python tiene incorporado un mecanismo para atrapar errores de distintos tipos, así como para generar errores que den información al usuario sobre usos incorrectos del código.

En primer lugar consideremos lo que se llama un error de sintaxis. El siguiente comando es sintácticamente correcto y el intérprete sabe como leerlo

print("hola")
hola

mientras que, si escribimos algo que no está permitido en el lenguaje

print("hola"))
  Cell In[2], line 1
    print("hola"))
                 ^
SyntaxError: unmatched ')'

El intérprete detecta el error y repite la línea donde lo identifica. Este tipo de errores debe corregirse para poder seguir con el programa.

Consideremos ahora el código siguiente, que es sintácticamente correcto pero igualmente causa un error

a = 1
b = 0
z = a / b
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

Cell In[3], line 3
      1 a = 1
      2 b = 0
----> 3 z = a / b


ZeroDivisionError: division by zero

Cuando se encuentra un error, Python muestra el lugar en que ocurre y de qué tipo de error se trata.

print(hola)
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

Cell In[4], line 1
----> 1 print(hola)


NameError: name 'hola' is not defined

Este mensaje da un tipo de error diferente. Ambos: ZeroDivisionError y NameError son tipos de errores (o excepciones). Hay una larga lista de tipos de errores que son parte del lenguaje y puede consultarse en la documentación de Built-in Exceptions.

Administración de excepciones

Cuando nuestro programa aumenta en complejidad, aumenta la posibilidad de encontrar errores. Esto se incrementa si se tiene que interactuar con otros usuarios o con datos externos. Consideremos el siguiente ejemplo simple:

with open("../data/ej_clase5.dat") as fi:
  for l in fi:
    t = l.split()
    print("t = {}".format(t))        # Línea sólo para inspección
    m = int(t[0])
    n = int(t[1])
    print(f"m = {m}, n = {n}, m x n = {m*n}")
print("Seguimos")
t = ['1', '2']
m = 1, n = 2, m x n = 2
t = ['2', '6']
m = 2, n = 6, m x n = 12
t = ['3', '9']
m = 3, n = 9, m x n = 27
t = ['4', '12']
m = 4, n = 12, m x n = 48
t = ['5.5', '30.25']
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

Cell In[5], line 5
      3 t = l.split()
      4 print("t = {}".format(t))        # Línea sólo para inspección
----> 5 m = int(t[0])
      6 n = int(t[1])
      7 print(f"m = {m}, n = {n}, m x n = {m*n}")


ValueError: invalid literal for int() with base 10: '5.5'

En este caso se “levanta” una excepción del tipo ValueError debido a que este valor (5.5) no se puede convertir a int. Podemos modificar nuestro programa para manejar este error:

with open("../data/ej_clase5.dat") as fi:
  for l in fi:
    t = l.split()
    try:
      m = int(t[0])
      n = int(t[1])
      print(f"m = {m}, n = {n}, m x n = {m*n}")
    except:
      print(f"Error: t = {t} no puede convertirse a entero")
print("Seguimos")
m = 1, n = 2, m x n = 2
m = 2, n = 6, m x n = 12
m = 3, n = 9, m x n = 27
m = 4, n = 12, m x n = 48
Error: t = ['5.5', '30.25'] no puede convertirse a entero
m = 3, n = 9, m x n = 27
Error: t = ['2'] no puede convertirse a entero
Seguimos

En este caso podríamos ser más precisos y especificar el tipo de excepción que estamos esperando

with open("../data/ej_clase5.dat") as fi:
  for l in fi:
    t = l.split()
    try:
      m = int(t[0])
      n = int(t[1])
      print(f"m = {m}, n = {n}, m x n = {m*n}")
    except(ValueError):
      print(f"Error: t = {t} no puede convertirse a entero")
m = 1, n = 2, m x n = 2
m = 2, n = 6, m x n = 12
m = 3, n = 9, m x n = 27
m = 4, n = 12, m x n = 48
Error: t = ['5.5', '30.25'] no puede convertirse a entero
m = 3, n = 9, m x n = 27
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

Cell In[12], line 6
      4 try:
      5   m = int(t[0])
----> 6   n = int(t[1])
      7   print(f"m = {m}, n = {n}, m x n = {m*n}")
      8 except(ValueError):


IndexError: list index out of range
with open("../data/ej_clase5.dat") as fi:
  for l in fi:
    t = l.split()
    try:
      m = int(t[0])
      n = int(t[1])
      print(f"m = {m}, n = {n}, m x n = {m*n}")
    except(ValueError):
      print(f"Error: t = {t} no puede convertirse a entero")
    except(IndexError):
      print(f'Error: La línea "{l.strip()}" no contiene un par')
print("Seguimos...")
m = 1, n = 2, m x n = 2
m = 2, n = 6, m x n = 12
m = 3, n = 9, m x n = 27
m = 4, n = 12, m x n = 48
Error: t = ['5.5', '30.25'] no puede convertirse a entero
m = 3, n = 9, m x n = 27
Error: La línea "2" no contiene un par
Seguimos...
with open("../data/ej_clase5.dat") as fi:
  for l in fi:
    t = l.split()
    try:
      m = int(t[0])
      n = int(t[1])
      print("m = {}, n = {}, m x n = {}".format(m,n, m*n))
    except(ValueError):
      print("Error: t = {} no puede convertirse a entero".format(t))
    except:
      print('Error: La línea "{}" tiene otro error'.format(l.strip()))
print("Seguimos...")
m = 1, n = 2, m x n = 2
m = 2, n = 6, m x n = 12
m = 3, n = 9, m x n = 27
m = 4, n = 12, m x n = 48
Error: t = ['5.5', '30.25'] no puede convertirse a entero
m = 3, n = 9, m x n = 27
Error: La línea "2" tiene otro error
Seguimos...

La forma general

La declaración try funciona de la siguiente manera:

  • Primero, se ejecuta el bloque try (el código entre las declaración try y except).

  • Si no ocurre ninguna excepción, el bloque except se saltea y termina la ejecución de la declaración try.

  • Si ocurre una excepción durante la ejecución del bloque try, el resto del bloque se saltea. Luego, si su tipo coincide con la excepción nombrada luego de la palabra reservada except, se ejecuta el bloque except, y la ejecución continúa luego de la declaración try.

  • Si ocurre una excepción que no coincide con la excepción nombrada en el except, esta se pasa a declaraciones try de más afuera; si no se encuentra nada que la maneje, es una excepción no manejada, y la ejecución se frena con un mensaje como los mostrados arriba.

El mecanismo es un poco más complejo, y permite un control más fino que lo descripto aquí.

“Crear” excepciones

Podemos forzar a que nuestro código cree una excepción usando raise. Por ejemplo:

import math
def mi_sqrt(x):
  if x < 0:
    raise ValueError(f"x = {x}, debería ser positivo")
  return math.sqrt(x)
mi_sqrt(12)
3.4641016151377544
mi_sqrt(-2)
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

Cell In[19], line 1
----> 1 mi_sqrt(-2)


Cell In[17], line 4, in mi_sqrt(x)
      2 def mi_sqrt(x):
      3   if x < 0:
----> 4     raise ValueError(f"x = {x}, debería ser positivo")
      5   return math.sqrt(x)


ValueError: x = -2, debería ser positivo
mi_sqrt(1+2j)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Cell In[20], line 1
----> 1 mi_sqrt(1+2j)


Cell In[17], line 3, in mi_sqrt(x)
      2 def mi_sqrt(x):
----> 3   if x < 0:
      4     raise ValueError(f"x = {x}, debería ser positivo")
      5   return math.sqrt(x)


TypeError: '<' not supported between instances of 'complex' and 'int'