Archivos y excepciones

¿Es lo mismo abrir un archivo en Windows que en Linux/macOS?

El proceso es diferente a la hora de especificar la ruta de directorios. Supongamos la siguiente estructura de directorio:

.
└── ejemplo/
    ├── programa.py
    ├── archivo1.txt
    └── carpeta/
        └── archivo2.txt

Si tenemos la terminal/línea de comandos posicionada sobre la carpeta ejemplo y queremos que nuestro programa.py pueda leer los archivos de texto:

  • No habría problemas con archivo1.txt, se llama a open('archivo1.txt').
  • Para abrir el archivo2.txt que se encuentra en carpeta, la ruta relativa completa debería ser:
    • open('carpeta/archivo2.txt') si se ejecuta el programa en Linux/macOS
    • open('carpeta\\archivo2.txt') si se ejecuta el programa en Windows.

Para que nuestro programa se ejecute sin problemas en todos los sistemas operativos, existe la función os.path.join que recibe una cantidad arbitraria de argumentos y los une con el separador correspondiente al sistema operativo de ejecución:

>>> import os
>>> os.path.join('carpeta1', 'carpeta2', 'archivo.txt')
'carpeta1/carpeta2/archivo.txt'

¿Cuál es la diferencia entre una ruta relativa y una absoluta?

Una ruta absoluta es el camino completo de directorios hasta llegar al archivo o directorio destino, comenzando desde el directorio raíz. Ejemplos:

En windows:
   C:\Documents\Algoritmos\Guia\Archivos\tail.py
   C:\Documents\Algoritmos\Guia\Archivos\archivo.txt
En macOS/linux/unix:
   /home/Documents/dessaya/Algoritmos/Guia/Archivos/tail.py
   /home/Documents/dessaya/Algoritmos/Guia/Archivos/archivo.txt

Una ruta relativa también es el camino hasta llegar a un archivo, pero va a depender de una ruta actual sobre la cuál estamos parados. Volviendo a los ejemplos anteriores, si tenemos la línea de comandos posicionada sobre el directorio Algoritmos, las rutas relativas para llegar al tail.py serían así:

En windows:
   Guia\Archivos\tail.py
En macOS, linux, sistemas unix:
   Guia/Archivos/tail.py

En cuanto al código, siempre se deben utilizar rutas relativas a la ruta del archivo principal de nuestro programa. El funcionamiento del mismo no debería depender de cuáles son las carpetas que se encuentran arriba. Si nuestro programa usa otros archivos, como por ejemplo .csv o imágenes, se pueden crear subcarpetas para estos (por ejemplo dejarlos en Archivos/recursos/) pero no deben encontrarse en directorios superiores.

Otro detalle importante a tener en cuenta: si nos encontramos sobre el directorio Algoritmos y ejecutamos el programa como python Guia/Archivos/tail.py, entonces las rutas relativas van a comenzar desde la carpeta Algoritmos, no sobre Archivos! Para solucionar esto debemos movernos hacia la carpeta Archivos y luego ejecutar normalmente python tail.py.

Estoy en Windows. ¿Cómo abro un archivo que se encuentra en una carpeta?

Para las rutas en Windows, los directorios se encuentran separados por \. El problema es que el caracter \ es especial y se usa en combinación con otros caracteres para definir saltos de líneas (\n), tabulaciones (\t) y más.

Con reemplazar cada ocurrencia de \ por un \\ alcanza para solucionarlo:

- ruta = 'archivos\informacion.csv'
+ ruta = 'archivos\\informacion.csv'`

¿Cómo puedo verificar si un archivo existe antes de abrirlo?

El módulo os provee muchas funciones para esto.

Para los fines de los trabajos prácticos, otra alternativa más simple es utilizar un try/except definiendo el error esperado. En este caso sería FileNotFoundError, o incluso otro más general como IOError.

¿Por qué se debe definir el error esperado en un try/except?

Supongamos un código muy simple:

def dividir(numerador, denominador):
    try:
        return numerador / denominador ""
    except:
        return None

Acá tenemos una funcionalidad acotada y tenemos en cuenta dónde podría suceder un posible error. Al definir la operación sabemos que estamos esperando un ZeroDivionError, entonces deberíamos incluirlo en el except!

En cualquier momento pueden ocurrir otros tipos de errores para los cuales nuestra función no estaría preparada para manejar. Solo para mencionar algunos:

  • KeyboardInterrupt si el usuario realiza Ctrl+C para salir del programa.
  • SystemExit si se realizan llamadas a exit().
  • MemoryError si el sistema se queda sin memoria.

Entonces para que estos errores no se vean accidentalmente atrapados por nuestro try/except definimos el error exacto que estamos esperando:

-   except:
+   except ZeroDivisionError:

¿Debería considerar la cantidad de llamadas a open?

Si. Por la jerarquía de memoria las llamadas a disco son más costosas que acceso a una variable en memoria. Considerando esto:

  • Si tenemos un archivo que entra en memoria cuya información no cambia y usamos de forma frecuente, se debería cargar ese archivo una vez en una estructura al comienzo del programa.
  • Si nuestro programa genera información para guardar en un archivo, no debería volver a generarse el archivo si la información a guardar no cambió.

¿Cómo se encara un problema donde el contenido de un archivo "no entran en memoria"?

Una forma clásica de procesar un archivo es primero cargarlo en memoria, generalmente en una estructura apta para el problema apto, y luego operar sobre el mismo. Con una limitación de memoria no podríamos realizar esto.

Que un archivo no entre en memoria no implica que no se puedan usar variables para guardar algo de su información, aunque no toda.

Va a depender del problema a resolver. Como alternativas podremos preguntarnos lo siguiente:

  • ¿Cuáles son las líneas relevantes del archivo?
  • ¿Hay algún tipo de filtrado que se podría aplicar al problema?
  • Mientras estamos recorriendo el archivo, ¿qué es lo mínimo indispensable a registrar para solucionar el problema? ¿Se puede descartar información vieja que ya no afecta al resultado final?

Tengo que definir el formato de un archivo, pero no se me ocurre cómo encararlo.

Como siempre depende del problema, pero hay dos características importantes a tener en cuenta:

  • El formato a elegir debe resultar en un proceso simple, tanto para leer como para escribir el archivo.
    • Una solución trivial para escribir el contenido de un diccionario sería usando un f.write(str(D)), pero esto va a resultar en una "lectura" del archivo rebuscada por la necesidad de separar y filtrar los caracteres irrelevantes de la representación de cadena del diccionario.
  • Considerar cuáles son los elementos necesarios a guardar.
    • En el caso de una lista de listas, si se tienen muchos casilleros vacíos no es relevante guardar esas posiciones en el archivo. Se podría guardar todo lo que no es vacío, y luego a la hora de cargar rellenar toda posición faltante con el vacío correspondiente del código.
  • Aprovechar saltos de línea y split.
    • Ambos permiten utilizar ciclos y estructurar la información de una forma esperada para luego cargarla.