Archivos en Python
Hasta ahora, hemos analizado cómo crear y trabajar con instancias de varios tipos de datos. Además, ya sabes cómo las estructuras de control pueden afectar el flujo del programa. Ahora es el momento de hacer un buen uso de este conocimiento y permitirte escribir programas más complejos.
Esta lección está dedicada a las diversas opciones que tienes para la entrada o salida de datos. Esto se refiere en particular a la lectura y escritura de archivos, que forma parte del repertorio estándar de un programador.
Antes de pasar específicamente a leer y escribir archivos en Python, la siguiente sección te presentará los conceptos básicos necesarios que te permitirán hacerlo.
Stream de datos
Un flujo o stream de datos es una secuencia continua de datos. Distinguimos entre dos tipos de flujos de datos: los datos se pueden leer de los flujos de datos entrantes (downstream) y escribirse en los flujos de datos salientes (upstream).
La salida de pantalla, la entrada de teclado y los archivos y conexiones de red se consideran flujos de datos. Por ejemplo, puedes imaginar que el programa lee un archivo e imprime en la pantalla un de cada dos palabras contenidas en el archivo.
Hay dos flujos de datos estándar que ya has utilizado sin saberlo: tanto la salida de una cadena a la pantalla como una entrada del usuario no son más que operaciones en los flujos de entrada y salida estándar, stdin
y stdout
, respectivamente. El flujo de salida estándar stdout
puede ser escrito utilizando la función de print
incorporada y leer desde el flujo de entrada estándar stdin
usando la función input
.
Todos los principales sistemas operativos te permiten abrir flujos de datos en modo texto o binario. Si bien generalmente no existe una diferencia técnica entre secuencias de texto y secuencias binarias, es importante abrir una secuencia usando el modo apropiado. En modo texto, ciertos caracteres de control, como por ejemplo los caracteres de nueva línea o el carácter de fin de archivo (EOF), pueden interpretarse según su respectiva semántica. Además, Python intenta automáticamente decodificar el contenido de los flujos de texto en instancias str
(tipo de dato de cadena de texto).
Como distinción final, existen flujos de datos en los que puedes posicionarte a voluntad y otros en los que no. Por ejemplo, un archivo representa un flujo de datos en el que la posición de lectura/escritura se puede especificar según se desee. Ejemplos de flujos en los que esto no funciona incluyen el flujo de entrada estándar (stdin
) y una conexión de red.
Leer datos de un archivo en Python
Primero, describiremos cómo puedes leer datos de un archivo. Para hacer esto, necesitamos acceder a ese archivo en modo de solo lectura. El archivo de prueba que usaremos en este ejemplo es un diccionario que contiene en cada línea una palabra en inglés y, separada por un espacio, su traducción al español. El archivo debe llamarse diccionario.txt
:
Spain España
Germany Alemania
Sweden Suecia
France Francia
Italy Italia
En el programa, queremos preparar los datos de este archivo para poder acceder a ellos cómodamente más adelante en un diccionario. Como pequeño extra, ampliaremos el programa para permitir que el usuario solicite la traducción de un término en inglés.
Abrir y cerrar un archivo
Primero, se debe abrir el archivo para su lectura. Para ello utilizamos la función incorporada open
. Esta función devuelve el llamado objeto de archivo:
file_object = open("diccionario.txt", "r")
Como primer parámetro de open
pasamos una cadena que contiene la ruta al archivo. Ten en cuenta que aquí se permiten rutas relativas y absolutas. El segundo parámetro también es una cadena de caracteres y especifica el modo en el que se abrirá el archivo, donde r
significa lectura y significa que el archivo se abre para lectura. Asociamos el objeto de archivo devuelto por la función con la referencia file_object
. Si el archivo no existe, se genera un FileNotFoundError
:
Traceback (most recent call last):
File "diccionario.py", line 1, in <module>
file_object = open("diccionario.txt", "r")
FileNotFoundError: [Errno 2] No such file or directory: 'diccionario.txt'
Una vez que se llama a open
, el objeto de archivo se puede usar para leer datos del archivo. Cuando el archivo ha sido leído, debe cerrarse explícitamente llamando al método close
:
file_object.close()
Después de llamar a este método, no se pueden leer más datos del objeto de archivo.
La declaración with
En la sección anterior, vio cómo puede abrir un archivo usando la función abierta incorporada y cerrarlo después de usar el método de cierre del objeto de archivo abierto:
file_object = open("diccionario.txt", "r")
# Hacer algo con file_object
file_object.close()
El uso de open
y close
es un patrón típico que encontrarás una y otra vez en diversas situaciones. Además de las operaciones con archivos, las conexiones de red, por ejemplo, también representan situaciones en las que primero se debe establecer una conexión, luego utilizarla y finalmente cerrarla.
El uso explícito de open
y close
, como se muestra en el ejemplo anterior, conlleva el riesgo de que debido a un error de programación o debido a la omisión del manejo de errores, no se llame al método close
y, por lo tanto, el objeto de archivo no se cierre. Para asegurarse de que no se produzcan tales errores, debes utilizar la instrucción with
para abrir un archivo:
with open("diccionario.txt", "r") as file_object:
# Hacer algo con file_object
pass
Tan pronto como el flujo de control abandona el bloque de instrucciones sangrado debajo de una instrucción with
, el objeto de archivo abierto con la instrucción with
se cierra automáticamente. En particular, esto también se aplica en caso de un error que no se haya solucionado.
Los objetos como el objeto de archivo que se pueden utilizar junto con una declaración with también se denominan administradores de contexto. En este punto, es suficiente saber que la declaración with garantiza que el administrador de contexto se cierre correctamente en todos los casos.
Leer el contenido del archivo
En el siguiente paso, queremos leer el archivo línea por línea. Esto es relativamente simple ya que el objeto de archivo se puede iterar línea por línea. Entonces podemos usar el viejo y familiar bucle for
:
with open("diccionario.txt", "r") as file_object:
for line in file_object:
print(line)
En el bucle for
, iteramos sobre el objeto de archivo línea por línea, y line
cada vez hace referencia al contenido de la línea actual. En este caso, cada línea del cuerpo del bucle simplemente se imprime. Sin embargo, queremos crear un diccionario en el programa que contenga los términos en inglés como claves y los respectivos términos en español como valores después de leer el archivo.
Para hacer esto, primero creamos un diccionario vacío:
words = {}
Luego, el archivo diccionario.txt
se abre para leerlo y se itera en un bucle sobre todas las líneas del archivo:
with open("diccionario.txt", "r") as file_object:
for line in file_object:
assignment = line.split(" ")
if len(assignment) == 2: # considerar solo líneas válidas
words[assignment[0]] = assignment[1]
En el cuerpo del bucle, ahora usamos el método split
para dividir la línea leída actualmente en dos partes de una lista: la parte a la izquierda del espacio (es decir, la palabra en inglés) y la parte a la derecha de el espacio, es decir, la palabra en español. En la siguiente línea del cuerpo del bucle, se crea una nueva entrada en el diccionario, con la clave de assignment[0]
(la palabra en inglés) y el valor de assignment[1]
(la palabra en español). Finalmente, decidimos saltarnos tácitamente una línea si no pudimos extraer exactamente dos componentes de ella. Elegimos este tipo de manejo de errores por razones didácticas para mantener el programa lo más simple posible. En la práctica, deberías preguntarte si deberías mostrar las líneas problemáticas o incluso cerrar el programa con un mensaje de error.
Ahora modifica el código anterior para que después de cerrar el objeto de archivo, el diccionario generado se imprima con la función print
. La salida se verá así:
{'Spain': 'España\n', 'Germany': 'Alemania\n', 'Sweden': 'Suecia\n', 'France': 'Francia\n', 'Italy': 'Italia\n'}
Puedes ver que después de cada valor hay un \n
, que es la secuencia de escape para un salto de línea. Esto se debe a que un salto de línea en Python se considera un carácter y, por tanto, parte del contenido del archivo. De este modo, cada línea de un archivo se lee en su totalidad, incluido un posible salto de línea al final. Por supuesto, el salto de línea sólo se lee si realmente existe.
No queremos volver a encontrar el salto de línea en el diccionario final. Por esta razón, llamamos al método strip
de la cadena de texto line
en cada iteración. Esto elimina todos los espacios en blanco, incluidos los saltos de línea, al principio y al final de la cadena:
words = {}
with open("diccionario.txt", "r") as file_object:
for line in file_object:
line = line.strip()
assignment = line.split(" ")
if len(assignment) == 2: # considerar solo líneas válidas
words[assignment[0]] = assignment[1]
De esta forma, el contenido del archivo se ha transferido completamente a un diccionario.
Como pequeño extra, ahora queremos permitir que el usuario envíe solicitudes de traducción al programa. La secuencia de flujo, debería verse así:
Introduce una palabra: Germany
La palabra en español es: Alemania
Introduce una palabra: Italy
La palabra en español es: Italia
Introduce una palabra: Greece
La palabra es unknown
En el programa leemos las solicitudes del usuario en un bucle infinito. El operador in
nos permite comprobar si la palabra leída existe como clave en el diccionario. En caso afirmativo, se emitirá la correspondiente traducción al español. Si la palabra ingresada no existe, se mostrará un mensaje de error:
words = {}
with open("diccionario.txt", "r") as file_object:
for line in file_object:
line = line.strip()
assignment = line.split(" ")
if len(assignment) == 2: # considerar solo líneas válidas
words[assignment[0]] = assignment[1]
while True:
word = input("Introduce una palabra: ")
if word in words:
print("La palabra en español es:", words[word])
else:
print("La palabra es unknown")
El programa de muestra presentado aquí está lejos de ser perfecto, pero muestra muy bien cómo los objetos de archivo y también los diccionarios se pueden usar de manera significativa.
No dudes en ampliar el programa. Por ejemplo, podrías permitir al usuario salir del programa correctamente, ofrecer traducciones en ambas direcciones o permitir el uso de múltiples archivos fuente.
Escribir datos en un archivo en Python
En la sección anterior, nos centramos en la lectura de archivos. El hecho de que también funcione al revés es el tema de esta sección. Para abrir un archivo para escribir, también utilizamos la función incorporada open
. Recordarás que esta función espera un modo como segundo parámetro, que tenía que ser r
para lectura. De manera similar, se debe especificar w
(write) si el archivo se va a abrir para escritura. Si el archivo deseado ya existe, se borrará. En cualquier caso se creará un nuevo archivo:
file_object = open("salida.txt", "w")
Después de que se hayan escrito todos los datos en el archivo, el objeto del archivo debe cerrarse llamando al método close:
file_object.close()
Además, al escribir un archivo, debes usar la instrucción with
en lugar de usar explícitamente open
y close
:
with open("salida.txt", "w") as file_object:
# Hacer algo con file_object
pass
Para escribir una cadena en el archivo abierto, puedes llamar al método write
del objeto de archivo. El siguiente programa de ejemplo pretende ser una contraparte del ejemplo de la última lección. Suponemos que palabras hace referencia a un diccionario que contiene términos en inglés como claves y las traducciones en español como valores, por ejemplo:
words = {
"Germany": "Alemania",
"Spain": "España",
"Greece": "Grecia"
}
Entonces es un diccionario como el creado por el programa de ejemplo de la sección anterior:
with open("salida.txt", "w") as file_object:
for word in words:
file_object.write(f"{word} {words[word]}\n")
Primero, abrimos un archivo llamado salida.txt
para escribir y luego iteramos sobre todas las claves del diccionario de palabras. En cada iteración, se escribe una cadena con el formato correspondiente en el archivo mediante file_object.write
. Ten en cuenta que al escribir un archivo, debes saltar explícitamente a una nueva línea agregando \n
.
Más adelante echaremos un vistazo más de cerca a la sintaxis de cadenas f"..."
utilizadas para formatear cadenas de texto. En este punto, debería ser suficiente para nosotros saber que una cadena f"..."
admite la definición de marcadores de posición en la cadena que se reemplazan por los valores de las expresiones dadas entre llaves.
El archivo escrito en este ejemplo se puede volver a leer con el programa de ejemplo de la lección anterior Leer datos de un archivo en Python.
Generar archivos en Python
Como puedes ver en los ejemplos anteriores, se puede crear un objeto de archivo utilizando la función incorporada open
. A esta función puedes pasarle otros parámetros además del nombre del archivo y el modo. Además, además de los modos r
y w
ya descritos, hay algunos otros que analizaremos más adelante. Finalmente, proporcionaremos una descripción general de los métodos del objeto de archivo resultante.
Nota
Esta sección contiene información detallada sobre los objetos de archivos y sirve principalmente como referencia, y por lo tanto, podrías saltártelos durante tu primera lectura.
La función incorporada open
La función open
abre un archivo y devuelve el objeto de archivo creado. Utilizando este objeto de archivo, posteriormente puedes realizar las operaciones necesarias en el archivo.
open(filename, [mode, buffering, encoding, errors, newline])
Ya hemos discutido los dos primeros parámetros en las secciones anteriores. Estos son el nombre del archivo o la ruta al archivo que se abrirá y el modo en el que se abrirá el archivo. Se debe pasar una cadena para el parámetro mode
. Todos los valores válidos y sus significados se enumeran en la siguiente tabla.
Modo | Descripción |
---|---|
r | El archivo se abre para lectura únicamente. |
w | El archivo se abre sólo para escritura. Se sobrescribirá cualquier archivo existente con el mismo nombre. |
a | El archivo se abre sólo para escritura. Un archivo posiblemente existente con el mismo nombre no se sobrescribirá, sino que se ampliará. |
x | El archivo se abre sólo para escritura, si aún no existe. Si ya existe un archivo con el mismo nombre, se genera una excepción FileExistsError . |
r+ , w+ , a+ , x+ | El archivo se abre para lectura y escritura. Ten en cuenta que w+ vaciará cualquier archivo existente con el mismo nombre. |
rb , wb , ab , xb , r+b , w+b , a+b , x+b | El archivo se abrirá en modo binario. Ten en cuenta que en este caso, se deben utilizar instancias bytes en lugar de cadenas, como veremos en lecciones posteriores. |
El parámetro mode
es opcional y se asigna el valor r
si se omite.
Los cuatro parámetros opcionales adicionales (buffering
, encoding
, errors
, y newline
) generalmente no son necesarios e ilustraremos su uso con varias interfaces similares a lo largo del curso. No obstante, queremos hacer un breve resumen de su significado llegados a este punto.
El cuarto parámetro opcional, encoding
, se puede utilizar para especificar la codificación en la que se leerá o escribirá el archivo. La codificación determina cómo se almacenan los caracteres especiales más allá del juego de caracteres ASCII. Especificar una codificación no tiene sentido al abrir un archivo en modo binario y debe omitirse en ese caso.
El quinto parámetro, errors
, determina cómo tratar los errores al codificar caracteres en la codificación especificada. Si se pasa el valor ignore
, los errores se ignorarán. Se genera una excepción ValueError
para un valor strict
, lo que también ocurre si no especificas el parámetro.
El parámetro buffering
controla el tamaño del buffer interno y newline
especifica los caracteres que se reconocerán o utilizarán como caracteres de nueva línea al leer o escribir el archivo.
Atributos y métodos de un objeto de archivo
Los parámetros especificados al abrir se pueden leer nuevamente a través de los atributos name
, encoding
, errors
, mode
, y newlines
del objeto de archivo resultante.
La siguiente tabla resume brevemente los métodos más importantes de un objeto de archivo. Analizaremos los métodos seek
y tell
con más detalle en la siguiente sección.
Método | Descripción |
---|---|
close() | Cierra un objeto de archivo existente. Ten en cuenta que después de eso no se pueden realizar operaciones de lectura o escritura. |
fileno() | Devuelve el descriptor⁽¹⁾ del archivo abierto como un número entero. |
flush() | Limpia el buffer interno.⁽²⁾ |
isatty() | True si el objeto de archivo se abrió en un stream de datos que no se puede escribir ni leer en ninguna posición. |
next() | Lee la siguiente línea del archivo y la devuelve como una cadena. |
read([size]) | Lee size bytes del archivo. Si no se especifica el tamaño o el archivo es menor que los bytes de tamaño, el archivo se leerá en su totalidad. Los datos se devuelven como una cadena o una cadena de bytes según el modo de lectura. |
readline([size]) | Lee una línea del archivo. El número de bytes que se leerán se puede limitar especificando size . |
readlines([sizehint]) | Lee todas las líneas y las devuelve como una lista de cadenas. Si se especifica sizehint , el proceso de lectura se ejecuta hasta que se hayan leído aproximadamente los bytes de sizehint .⁽³⁾ |
seek(offset,[whence]) | Establece la posición actual de lectura/escritura en el archivo a offset . |
tell() | Devuelve la posición actual de lectura/escritura en el archivo. |
truncate([size]) | Elimina todos los datos del archivo después de la posición actual de lectura/escritura o, si se especifica, todo excepto los primeros size bytes. |
write(str) | Escribe la cadena str en el archivo. |
writelines(iterable) | Escribe varias líneas en el archivo. El objeto iterable iterable debe pasar a través de cadenas de texto, como una lista de cadenas, por ejemplo. |
⁽¹⁾ Un descriptor de archivo es un número de identificación asignado por el sistema operativo para archivos abiertos. Los streams stdin
y stdout
predeterminados tienen descriptores 0
y 1
, respectivamente.
⁽²⁾ El sistema operativo puede almacenar en buffer las operaciones de archivos pendientes para mayor eficiencia y ejecutarlas más adelante. Esta es la razón, por ejemplo, por la que no se deben extraer las memorias USB sin cerrar sesión en el sistema operativo.
⁽³⁾ En este contexto, "aproximadamente" significa que el número de bytes a leer se puede redondear al tamaño del buffer interno.
Cambiar la posición de escritura/lectura
Los ejemplos anteriores han mostrado cómo se pueden leer o escribir archivos de forma secuencial. Debido a la naturaleza especial de los archivos, es posible cambiar la posición de escritura o lectura a voluntad. Para ello, puede utilizar los métodos seek
y tell
del objeto de archivo.
seek(offset, [whence])
El método seek
de un objeto de archivo establece la posición de lectura/escritura dentro del archivo. Es la contraparte del método tell
, que devuelve la posición actual de lectura/escritura.
Nota
El método
seek
no tiene ningún efecto en el modoa
. En el modoa+
, la posición de lectura/escritura se cambia para que pueda leerse en cualquier punto del archivo, pero se restablece antes de una operación de escritura.
Si el archivo se abrió en modo binario, el parámetro offset
se cuenta en bytes desde el principio del archivo. Esta interpretación de offset
puede verse influenciada por el parámetro opcional whence
(consulte la siguiente tabla).
Valor de whence | Interpretación de offset |
---|---|
0 | Número de bytes relativos al comienzo del archivo. |
1 | Número de bytes relativos a la posición actual de lectura/escritura |
2 | Número de bytes relativos al final del archivo |
No podrás utilizar el método seek
con tanta libertad si el archivo se abre en modo texto. Aquí, sólo se deben utilizar como offset
los valores de retorno del método tell
. Los valores diferentes pueden provocar un comportamiento indefinido.
En el siguiente ejemplo, el método seek
se utiliza para determinar el ancho, el alto y la profundidad del color de un gráfico de mapa de bits:
from struct import unpack
with open("imagen.bmp", "rb") as f:
f.seek(18)
width, height = unpack("ii", f.read(8))
f.seek(2, 1)
bpp = unpack("H", f.read(2))[0]
print("Ancho:", width, "px")
print("Alto:", height, "px")
print("Profundidad del color:", bpp, "bpp")
De la especificación del formato del archivo de mapa de bits, podemos ver que la información que estamos buscando se encuentra en los offsets 18
, 22
y 28
, en forma de dos valores consecutivos de cuatro bytes y un valor de dos bytes. Por lo tanto, abrimos el archivo imagen.bmp
para leerlo en modo binario y omitimos los primeros 18
bytes usando el método seek
. En este punto, podemos usar read
para leer el ancho y alto del gráfico. Los valores leídos mediante read
se devuelven como una cadena de bytes y, por lo tanto, deben convertirse para nuestros propósitos en números. Para hacer eso, usamos la función unpack
del módulo struct
de la biblioteca estándar. La declaración de formato ii
requerida por unpack
establece que la cadena de bytes pasada debe interpretarse como dos enteros consecutivos de 32 bits con signo.
Después de leer el ancho y el alto, saltamos dos bytes más de la posición de lectura actual (el parámetro whence
se establece en 1
en la llamada de seek
) y luego podemos leer dos bytes que contienen la profundidad de color de la imagen. Finalmente, se imprime la información leída. La salida en pantalla del programa de gráficos debe verse similar a lo siguiente:
Ancho: 800 px
Alto: 600 px
Profundidad del color: 24 bpp