Clase 7: Persistencia de datos y módulos de biblioteca standard


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
s[:20]
'AaanAaronnAbanAbaban'

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

print(type(lines))
print(len(lines))
<class 'list'>
1585
lines[:3]
['Aaan', 'Aaronn', 'Aban']
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

"""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(f"({n:02d}): {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

Módulo Pathlib

En la versión de Python 3.4 se agregó un módulo con definiciones de clases para representar paths y archivos, con representaciones para los distintos filesystems.

from pathlib import Path
mi_path = Path()
home = mi_path.home()
here = Path(".")
print(home)
print(here)
/home/fiol
.
parent = here / ".."
print(parent)
..
type(parent)
pathlib._local.PosixPath
print(parent.resolve())
/home/fiol/Clases/IntPython/clases-python

Métodos y propiedades

El ejemplo anterior usa el método resolve() del objeto Path(). Veamos algunos otros:

# here y parent son ahora path completos
here = here.resolve()
parent = parent.resolve()
here
PosixPath('/home/fiol/Clases/IntPython/clases-python/clases')
parent
PosixPath('/home/fiol/Clases/IntPython/clases-python')
here.parent  # Propiedad
PosixPath('/home/fiol/Clases/IntPython/clases-python')

Partes del path

here.parts
('/', 'home', 'fiol', 'Clases', 'IntPython', 'clases-python', 'clases')

Podemos acceder a todas las carpetas que contienen el path actual simplemente iterando

for up in here.parents:
    print(up)
/home/fiol/Clases/IntPython/clases-python
/home/fiol/Clases/IntPython
/home/fiol/Clases
/home/fiol
/home
/
p = here / "07_1_inout.ipynb"
print(f'pathname : {p}')
print(f'path     : {p.parent}')
print(f'name     : {p.name}')
print(f'stem     : {p.stem}')
print(f'suffix   : {p.suffix}')
pathname : /home/fiol/Clases/IntPython/clases-python/clases/07_1_inout.ipynb
path     : /home/fiol/Clases/IntPython/clases-python/clases
name     : 07_1_inout.ipynb
stem     : 07_1_inout
suffix   : .ipynb

Contenido de directorios

print(here)
/home/fiol/Clases/IntPython/clases-python/clases
for f in here.glob('0?_*.ipynb'):
    print(f)

El objeto tiene un iterador que nos permite recorrer todo el directorio. Por ejemplo si queremos listar todos los subdirectorios:

direct = heredd
[x for x in here.iterdir() if x.is_dir()]

Trabajo con rutas de archivos

print(here.absolute())
/home/fiol/Clases/IntPython/clases-python/clases

Podemos reemplazar el módulo glob utilizando este objeto:

for fi in sorted(here.glob("0[1-7]_1*.ipynb") ):
    print(fi)
/home/fiol/Clases/IntPython/clases-python/clases/01_1_instala_y_uso.ipynb
/home/fiol/Clases/IntPython/clases-python/clases/02_1_modular.ipynb
/home/fiol/Clases/IntPython/clases-python/clases/03_1_maslistas.ipynb
/home/fiol/Clases/IntPython/clases-python/clases/04_1_funciones.ipynb
/home/fiol/Clases/IntPython/clases-python/clases/04_1_iteraciones_func.ipynb
/home/fiol/Clases/IntPython/clases-python/clases/05_1_func_func.ipynb
/home/fiol/Clases/IntPython/clases-python/clases/06_1_excepciones.ipynb
/home/fiol/Clases/IntPython/clases-python/clases/07_1_decoradores_old.ipynb
/home/fiol/Clases/IntPython/clases-python/clases/07_1_inout.ipynb
fi = here / "01_1_instala_y_uso.rst"
if fi.exists():
    s= fi.read_text()
    print(s[:300])
Introducción al lenguaje
========================

Cómo empezar: Instalación y uso
-------------------------------

Python es un lenguaje de programación interpretado, que se puede
ejecutar sobre distintos sistemas operativos, es decir, es
multiplataforma (suele usarse el término *cross-platform
print(fi.exists())
True

Leer un archivo

datos = here / 'data' / "names.txt"
type(datos)
pathlib._local.PosixPath
datos.exists()
True
s = datos.read_text()
print(s[:50])
Aaa
Aaron
Aba
Ababa
Ada
Ada
Adam
Adlai
Adrian
Adri

En este ejemplo leímos todo el archivo con un comando. En algunas ocasiones, por ejemplo si el archivo es muy largo, es preferible cada línea y procesarla antes de pasar a la siguiente. El ejemplo anterior puede escribirse utilizando este módulo de una manera muy similar:

"""Programa para contar e imprimir las palabras de una longitud dada"""
from pathlib import Path
fname = Path(".")/ 'data'/'names.txt'
output = fname.parent / f"{fname.stem}_palindromo{fname.suffix}"
n = 0                           # contador
minlen = 3                      # longitud mínima
maxlen = 4                      # longitud máxima
L = []
# with fname.open('r') as fi:
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 output.open(mode='w') as fo:
#    fo.write(ss)
output.write_text(ss)
Encontramos un total de 56 palabras capicúa que tienen entre 3 y 4 letras
234

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()
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']
l[0]
b'xc3x81frica'

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
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()

String, bytes y codificaciones

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

l[0] = b'\xc3\x81frica'

Esto indica que la variable es del tipo “bytes”.

Para todo tipo de variables, y en todos los lenguajes de programación, tenemos -por necesidad- dos representaciones:

  1. La representación que se hace en memoria, que consiste en una cadena de unos y ceros

  2. La representación que vemos nosotros, que depende del tipo de variable.

Para cada tipo de variable existe una convención de qué significa la cadena de 1s y 0s. Por ejemplo, para un número entero como el 3, la representación interna es 11.

En la memoria solamente se pueden guardar bytes, entonces para guardar cualquier tipo de dato debemos además de guardar la cadena de unos y ceros, tener la información de cual es la convención para codificarlo (codificación o encoding). Esto ocurre por ejemplo si queremos guardar una imagen, debemos convertirlo a una cadena de bytes utilizando algún tipo de encoding usando una convención (de jpg, o png, o …).

En el caso de los números, enteros o punto flotante, la codificación que suelen utilizar los lenguajes de programación es bastante standard (IEEE), pero en el caso de lenguaje, ha ido evolucionando a lo largo del tiempo y las diferencias en las distintas convenciones son un poco más visibles.

Históricamente, los lenguajes de programación borronearon la distinción entre los caracteres y la manera de guardar estos caracteres, igualando implícita o explícitamente la secuencia de bytes con un caracter en la codificación ASCII.

En Python 3, existen las dos representaciones de un caracter: - Un byte string es una secuencia de bytes, necesario para guardar en una computadora y no para leer por personas. - Un character string, llamado usualmente “string”, es una secuencia de caracteres que podemos leer pero que para guardar en memoria tiene que ser convertido a byte string utilizando una convención (encoding). Hay muchas convenciones, entre ellas ASCII o UTF-8.

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

El encoding es entonces nada más (y nada menos) que la convención que vamos a utilizar para interpretar una cadena de bytes. Entonces, si utilizamos dos convenciones diferentes para la misma cadena de bytes podemos obtener diferentes palabras

mis_bytes = b'\xcf\x84o\xcf\x81\xce\xbdo\xcf\x83'
type(mis_bytes)
bytes
list(mis_bytes)
[207, 132, 111, 207, 129, 206, 189, 111, 207, 131]
mis_bytes.decode('utf-16')
'蓏콯캁澽菏'
mis_bytes.decode('utf-8')
'τoρνoσ'
list(mis_bytes.decode('utf-8'))
['τ', 'o', 'ρ', 'ν', 'o', 'σ']
type(mis_bytes.decode('utf-8'))
str

Volviendo a la lista de palabras que leemos del archivo, para convertir de bytes a string utilizamos el método decode() con la codificación adecuada:

l[0]
b'xc3x81frica'
list(l[0])
[195, 129, 102, 114, 105, 99, 97]
l[0].decode('utf-8').encode()
b'xc3x81frica'
l[0].decode('utf-8').encode('utf-16')
b'xffxfexc1x00fx00rx00ix00cx00ax00'

Ejercicios 07 (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.dat” (así como otros archivos usados en las clases) pueden encontrarse en la carpeta intro-python