C: Ficheros, ejecución de scripts fuera de JupyterLab y ejecución de programas externos

Trabajar con ficheros

En Python podemos encontrar varias funciones que nos permiten trabajar con ficheros: podemos abrirlos, crearlos, leerlos, escribirlos, borrarlos, moverlos, obtener meta-información sobre ellos, etc. Veámoslo con ejemplos:

[1]:
f = open("salidas/miFichero.txt", "w")

f.write("Grabamos una cadena de texto en un fichero.\n")
f.write("Y otra cadena más")

f.close()
  • La función open() abre un fichero llamado miFichero.txt con el modo de «escribir» ("w") en él. Devuelve un objeto de tipo file. Los modos básicos de apertura de ficheros son:

    • «w»: Modo escritura (crea el fichero si no existe. Si existe borra el contenido)

    • «r»: Modo lectura

    • «a»: Modo añadir (crea el fichero si no existe. Si existe se añadirá el contenido al final).

    • En realidad hay más modos de apertura y modificadores.

  • .write() es un método que escribirá la cadena que pasemos como parámetro en el fichero.

  • .close() es el método que cierra el fichero y se asegura que los datos se terminan de escribir en el archivo (flush). Es MUY importante cuando trabajemos con archivos cerrarlos para asegurarnos su integridad.

[2]:
f = open("salidas/miFichero.txt", "r")

contenidoFichero = f.read()

f.close()

print(contenidoFichero)
Grabamos una cadena de texto en un fichero.
Y otra cadena más
  • .read() es el método que permite leer el contenido de un fichero y lo devuelve como una cadena.

[3]:
f = open("salidas/miFichero.txt", "r")

lineas = f.readlines()

f.close()

print(lineas)
['Grabamos una cadena de texto en un fichero.\n', 'Y otra cadena más']
  • .readlines() es el método que lee el fichero pero lo devuelve como una lista de cadenas de caracteres (una por línea).

[4]:
f = open("salidas/miFichero.txt", "r")

linea = f.readline()

while linea != "":
    print(linea)

    linea = f.readline()

f.close()
Grabamos una cadena de texto en un fichero.

Y otra cadena más
  • .readLine() es el método que nos permite ir leyendo el fichero línea a línea, lo que puede ser útil para ficheros muy grandes que no queramos cargar de una sola vez.


Ejercicio C.1:

Hagamos un script que pregunte al usuario por una cadena de texto y te diga si esa palabra es una palabra que esté en el diccionario de la RAE o no. Para eso podemos bajarnos el fichero .txt con todas las palabras de dicho diccionario de este repositorio. Por cierto que en dicho repositorio el autor cuenta como ha obtenido dicha lista de palabras (un ejercicio de scraping interesante, que veremos más adelante). Si tienes problemas al visualizar las palabras que contiene ese fichero (tema tildes, etc), mira la sección codificación de caracteres.

[5]:
# Tu código aquí

Ejercicio C.2: (¡no resuelto!)

Modifica el ejercicio B.12 para que la palabra aleatoria sea cualquiera del diccionario de la RAE. Lo mismo tienes problemas con los acentos, diéresis, etc… así que habrá que tener cuidado :-)

[6]:
# Tu código aquí

Gestión de excepciones

En el siguiente código la instrucción falla porque el fichero no existe y Python lanza una excepción:

[7]:
f = open("ficheroQueNoExiste.txt", "r")
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[7], line 1
----> 1 f = open("ficheroQueNoExiste.txt", "r")

File ~/anaconda3/envs/cursoAstronomia2/lib/python3.8/site-packages/IPython/core/interactiveshell.py:284, in _modified_open(file, *args, **kwargs)
    277 if file in {0, 1, 2}:
    278     raise ValueError(
    279         f"IPython won't let you open fd={file} by default "
    280         "as it is likely to crash IPython. If you know what you are doing, "
    281         "you can use builtins' open."
    282     )
--> 284 return io_open(file, *args, **kwargs)

FileNotFoundError: [Errno 2] No such file or directory: 'ficheroQueNoExiste.txt'

Las excepciones pueden saltar por muchos motivos (problemas al leer / escribir archivos, división por 0, intentar acceder a una posición de una lista que no existe)… Y podemos controlar el flujo de nuestro programa por si aparece alguna excepción (especialmente en los sitios donde sospechamos que se puede producir un error. Es el caso típico del manejo de ficheros, porque puede que no se pueda crear un fichero, que el disco esté lleno, que el fichero sea de solo lectura, etc. Ejemplo para gestionar las excepciones:

[8]:
try:
    f = open("salidas/otroFichero.txt")
    try:
        f.write("Algo que quiero escribir")
    except:
        print("Algo pasó cuando se escribía en el fichero")
    else:
        print("Parece que salió bien")
    finally:
        f.close()
except:
    print("Algó pasó cuando se abría el fichero")
Algó pasó cuando se abría el fichero
  • try es una palabra reservada que inicia un bloque donde se van a controlar posibles excepciones.

  • except es una palabra reservada que indica que bloque ejecutar cuando salte una excepción.

  • else es una palabra reservada que indica que bloque ejecutar cuando NO ha saltado una excepción en el try.

  • finally es una palabra que indica que bloque ejecutar siempre, ocurra o no una excepción.

En el caso de manejo de archivos es importante que añadamos un finally para asegurarnos que cerraremos el fichero siempre. Si no, podemos acabar con muchos ficheros «mal cerrados». Y eso no es bueno.

[9]:
try:
    val = 7 / 0
except ZeroDivisionError as err:
    print(f"Ha ocurrido una division por 0: {err}")

print("¡Pero el programa ha continuado!")
Ha ocurrido una division por 0: division by zero
¡Pero el programa ha continuado!

En este caso hemos visto como podemos identificar cada tipo de excepción que sospechemos que puede ocurrirnos. De hecho podemos tener varios bloques except atendiendo distintas excepciones en un try.

Si quisieramos capturar cualquier excepción y tener información sobre la misma podemos usar en la clausula except la clase de la que heredan todas las excepciones en Python: BaseException:

[10]:
try:
    val = 7 / 0
except BaseException as err:
    print(f"Ha ocurrido una division por 0: {err}")
Ha ocurrido una division por 0: division by zero

La gestión de excepciones puede ser más compleja.

Para hacer un poco más fácil el manejo de ficheros, podemos usar la clausula with:

[11]:
with open('salidas/miFichero.txt', 'w') as fichero:
    fichero.write("Escribiendo en el fichero\n")
    fichero.write("Y otra línea...\n")

Con esta sintaxis después del bloque que inicia la palabra reservada with el fichero se cerrará correctamente.

Ficheros binarios

Hasta ahora hemos creado y leido ficheros de texto. Pero también podríamos manejar ficheros binarios (donde la información se guarda como bytes). Para muchos tipos de ficheros normalmente existen bibliotecas que nos permitirán leerlos de manera más adecuada… pero si realmente queremos escribir «byte a byte» un fichero, podemos hacerlo.

En este ejemplo vamos a escribir una imagen muy simple en el formato PGM binario. Basicamente es un formato en el que se escribe de la siguiente manera:

  • P5: Identificador del tipo de imagen

  • _: espacio en blanco

  • Anchura de la imagen en pixeles (en modo texto)

  • _: espacio en blanco

  • Altura de la imagen en pixeles (en modo texto)

  • _: espacio en blanco

  • Máximo valor del pixel (color blanco) (en modo texto)

  • _: espacio en blanco

  • Una lista de bytes de tamaño anchura x altura con los valores de los píxeles de la imagen.

[12]:
informacion = 'P5 9 9 255 '

filas = []
filas.append( [0,0,0,0,255,0,0,0,0] )
filas.append( [0,0,0,0,255,0,0,0,0] )
filas.append( [0,0,100,0,255,0,0,0,0] )
filas.append( [0,0,0,0,255,0,0,0,0] )
filas.append( [255,255,255,255,255,255,255,255,255] )
filas.append( [0,0,0,0,255,0,0,0,0] )
filas.append( [0,0,0,0,255,0,0,0,0] )
filas.append( [0,0,0,0,255,0,0,0,0] )
filas.append( [0,0,0,0,255,0,0,0,0] )



fichero = open("salidas/imagen.pgm", "wb")   # El modificador ```b``` especifica que el fichero es binario

b = bytes(informacion, encoding="ASCII")
fichero.write(b)

for f in filas:
    b = bytes(f)
    fichero.write(b)

fichero.close()

Ejercicio C.3: (no resuelto)

Modifica el ejercicio B.13 para que el fractal de Mandelbrot se guarde como una imagen de tipo PGM. Tendrás que obtener una imagen más o menos así:

_images/fractal.png

Ejercicio C.4: (no resuelto)

Modifica el ejercicio C.3 para que el fractal de Mandelbrot para añadir una condición de parada y poder pintar los puntos de fuera del conjunto de Mandelbrot de una manera más bonita. Se trata de parar la iteración cuando \(|z| > 2\). En ese caso el punto no es del conjunto de Mandelbrot y podemos pintar ese pixel con el valor = al número de iteraciones que hemos hecho. Puedes consultar esto para algo más de información. Tendrás que obtener una imagen más o menos así:

_images/fractalGris.png

Otras operaciones con ficheros

Exites otras operaciones que pueden resultar de interés para operar con ficheros, como por ejemplo comprobar si un fichero existe, su tamaño, crear carpetas, renombrarlos, etc. Veamos algunos ejemplos:

[13]:
import os.path

print(os.path.exists("ficherosAuxiliares/fractal.png"))   # Comprobamos que un fichero existe

print(os.path.exists("unFicheroQueNoExiste"))
True
False

También podemos usar la biblioteca pathlib (enlace):

[14]:
from pathlib import Path

pfractal = Path('ficherosAuxiliares/fractal.png')

print(pfractal.exists())   # Otra manera de saber si existe
True
[15]:
directorio = Path('ficherosAuxiliares')

print(directorio.exists())

print(directorio.is_file())     # Para saber si es un fichero

print(directorio.is_dir())      # Para saber si es un directorio
True
False
True
[16]:
print(pfractal.resolve())   # Obtener la ruta absoluta del fichero
/home/zerjillo/MEGA/cursoAstronomia/notebooks/ficherosAuxiliares/fractal.png
[17]:
nuevoDirectorio = Path("salidas/nuevoDirectorio")
Path.mkdir(nuevoDirectorio)  # Para crear un directorio
[18]:
nuevoNombre = Path("salidas/otroDirectorio")
nuevoDirectorio.rename(nuevoNombre)
[18]:
PosixPath('salidas/otroDirectorio')
[19]:
Path.rmdir(nuevoNombre)  # Para borrar un directorio
[20]:
import shutil  # Para copiar un fichero

shutil.copyfile('ficherosAuxiliares/fractal.png', 'salidas/fractalCopiado.png')
[20]:
'salidas/fractalCopiado.png'
[21]:
directorio = Path('ficherosAuxiliares')

for hijo in directorio.iterdir():  # Iteramos por todos los ficheros (y directorios) de un directorio
    print(hijo)
ficherosAuxiliares/fractal2.png
ficherosAuxiliares/fractal.png
ficherosAuxiliares/mueveMotor.py
ficherosAuxiliares/ESP32_motor_real.jpg
ficherosAuxiliares/portadaWebScraping.jpg
ficherosAuxiliares/AccelStepper.py
ficherosAuxiliares/INDI_kstars_03.jpg
ficherosAuxiliares/fractalGris.png
ficherosAuxiliares/moduloGPS.jpg
ficherosAuxiliares/webScrapingAstronomia.pdf
ficherosAuxiliares/ocultacionTriton.png.svg
ficherosAuxiliares/ocultacionTritonAnotada.png
ficherosAuxiliares/data_cube.jpg
ficherosAuxiliares/ESP32_led.jpg
ficherosAuxiliares/miPrograma.py
ficherosAuxiliares/main.py
ficherosAuxiliares/controlDispositivosIndi.pdf
ficherosAuxiliares/INDI_kstars_02.jpg
ficherosAuxiliares/.ipynb_checkpoints
ficherosAuxiliares/the-spectral-data-cube.png
ficherosAuxiliares/ESP32.jpg
ficherosAuxiliares/ocultacionTriton.png
ficherosAuxiliares/portadaCurso.jpg
ficherosAuxiliares/AstronomiaPython.pdf
ficherosAuxiliares/portadaINDI.jpg
ficherosAuxiliares/ejemploArgsparser.py
ficherosAuxiliares/INDI_kstars_01.jpg
ficherosAuxiliares/ESP32_led_real.jpg
ficherosAuxiliares/stacking-images.jpg

Ejercicio C.5:

Lista todos los ficheros del directorio ficherosAuxiliares que tengan una extension .jpg o .png.

[22]:
# Tu código aquí

### Codificación de caracteres en los ficheros

La codificación de caracteres es el método que permite asignar caracteres (de un alfabeto concreto) a los códigos numéricos que realmente utiliza nuestro ordenador. Históricamente ha habido varias codificaciones de caracteres. Las primeras solo permitían usar caracteres que entendería un angloparlante, mientras que en los últimos años existen codificaciones (como Unicode) que permiten representar cualquier caracter de cualquier lengua.

En los sistemas operativos tipo UNIX (Linux, MacOs, etc) la codificación por defecto es la variante UTF-8 de Unicode. Sin embargo en Windows es posible que el sistema utilice una codificación distinta con lo que al acceder a determinados ficheros de texto los caracteres «especiales» (tildes, ñ, diéresis, etc.) pueden no visualizarse bien. Para esos casos en algunas operaciones con ficheros es importante especificar la codificación que debe usarse cuando abrimos el fichero. Eso lo podemos hacer con el parámetro opcional encoding:

[23]:
f = open("salidas/miFichero.txt", "r", encoding="utf-8")

Buscando un poco puedes encontrar más información al respecto.

Ejecución de programas Python fuera de JupyterLab

Ejecutar un programa o script de Python es muy sencillo. Dicho programa debe estar grabado en un fichero con extensión .py. Por ejemplo tenemos el fichero miPrograma.py (enlace) con el siguiente código:

[24]:
nombre = input("¿Cómo te llamas? ")

print(f"Encantado de conocerte {nombre}")
¿Cómo te llamas?  Zerjillo
Encantado de conocerte Zerjillo

Para ejecutar ese programa fuera de JupyterLab solo tenemos que abrir un prompt de Anaconda y con el entorno activado (conda activate cursoAstronomia) para asegurarnos que se ejecuta la versión correcta de python y que tenemos todas las bibliotecas que hemos instalado ejecutamos la orden:

> python miPrograma.py

Y el programa debe ejecutarse sin problemas.

Si necesitamos pasarle parámetros al programa se puede hacer de varias maneras, pero mi recomenación es utilizar una biblioteca de parseo de parámetros como por ejemplo `argparse <https://docs.python.org/3/library/argparse.html>`__. Podéis encontrar un tutorial al respecto. Aquí solo mostraremos un pequeño ejemplo:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("echo")

args = parser.parse_args()

print(args.echo)

El import nos permite usar el módulo. parser será nuestro objeto parseador, al cual le añadimos un argumento posible que se llama echo. El método parse_args() ejecuta el pareador con todos los argumentos que se hayan pasado y ya podemos acceder a ellos como si fueran propiedades del objeto args (args.echo). Si ejecutamos dicho script desde la línea de comandos:

> python ejemploArgsparser.py

Se quejará de que hay que pasarle un argumento llamado echo. Si llamamos al programa:

> python ejemploArgsparser.py LALALA

comprobaremos que efectivamente se parsea uno de los argumentos (el primero) y se asigna a args.echo, por lo que el programa imprime en la salida LALALA.

Iconitos y Windows: Si somos usuarios de Windows y necesitamos tener un iconito que al hacer doble click se nos ejecute nuestro programa podemos hacerlo, pero hay que hacer un poco de «magia negra». Podéis consultar los siguientes enlaces:

Ejecución de programas externos

En ocasiones ya existen programas o utilidades que hacen algo muy bien y no tiene sentido reinventar la rueda y programar esas utilidades desde 0 en Python. Lo que podemos hacer es llamar a dichas utilidades desde nuestro propio programa. Por ejemplo, si tenemos instaladas las utilidades de ImageMagick, que nos permiten convertir ficheros de imagen entre distintos formatos, modificar las imágenes, etc desde la línea de comandos podemos hacer uso de dichas utilidades desde nuestro script sin tener que instalar nada más.

Por ejemplo, si ejecuto:

> convert ficherosAuxiliares/fractal2.png -resize 25% ficherosAuxiliares/fractal2.jpg

Consigo que ImageMagick cree un fichero fractal2.jpg con formato JPEG a un cuarto del tamaño original a partir de una imagen fractal2.png en formato PNG.

Desde nuestro script podemos conseguir lo mismo:

[25]:
import os

os.system("convert ficherosAuxiliares/fractal2.png -resize 25% salidas/fractal2.jpg")
[25]:
0

Incluso podemos lanzar programas que tengan interfaz de usuario:

[26]:
import os

os.system("firefox")
[26]:
0

Es mejor práctica usar el módulo subprocess:

[27]:
import subprocess

subprocess.run(["convert", "ficherosAuxiliares/fractal2.png", "-resize", "25%", "salidas/fractal2.jpg"])
[27]:
CompletedProcess(args=['convert', 'ficherosAuxiliares/fractal2.png', '-resize', '25%', 'salidas/fractal2.jpg'], returncode=0)

Algunos enlaces al respecto:


Ejercicio C.6:

Transforma con el comando externo convert de ImageMagick todas las imagenes .jpg y .png que haya en el directorio ficherosAuxiliares haciéndolas en doble de grandes y cuyo nombre de archivo sea el nombre original añadiéndole _doble. Por ejemplo, el fichero fractal.png deberá llamarse fractal_doble.png. Los ficheros copiados deberán grabarse en la carpeta salidas.

[28]:
# Tu código aqui

Ejercicio C.7: (no resuelto)

Haz un programa que te diga paso a paso como solucionar el juego de las Torres de Hanói. Usando la recursividad la solución puede ser muy escueta.


Ejercicio C.8: (no resuelto)

Crea un script para generar una lista de n números aleatorios entre 0 y 100000000. Dicha lista debe poder grabarse en un fichero que se llame numeros.txt. Crea una función que permita leer dicho fichero de vuelta a una lista en memoria.


Ejercicio C.9: (no resuelto)

Ordenar una lista de números no es especialmente complejo. De hecho existen muchos métodos para ordenarla. Investiga como funciona el algoritmo de ordenación de burbuja, selección, inserción, quicksort… Programa cada uno de los algoritmos y ejecutalo sobre una lista de números aleatorios grande. Mide el tiempo que tarda cada algoritmo e imprímelo en pantalla.


Ejercicio C.10: (no resuelto)

Modifique las funciones del ejercicio anterior para que admitan como parámetro la función de comparación de manera que se puedan ordenar las listas siguiendo un criterio distinto (de mayor a menor, de menor a mayor) o incluso con datos que no sean numéricos y que, por defecto, no puedan compararse con los operadores típicos >, <, etc.


Ejercicio C.11: (no resuelto)

Supongamos que tenemos una función (del estilo a los objetos Polinomio que hemos hecho en ejercicios anteriores). Supongamos también que no sabemos resolver el problema de encontrar sus ceros analíticamente. Haz una función a la que le pases un objeto Polinomio y que mediante aproximaciones sucesivas sea capaz de decirte el punto xen el que la función vale 0.


Ejercicio C.12: (no resuelto)

Modifica el ejercicio anterio para encontrar por aproximaciones sucesivas un mínimo local de una función que le pasemos.