¿Cómo debo leer un archivo línea por línea en Python?

137

En tiempos prehistóricos (Python 1.4) hicimos:

fp = open('filename.txt')
while 1:
    line = fp.readline()
    if not line:
        break
    print line

después de Python 2.1, hicimos:

for line in open('filename.txt').xreadlines():
    print line

antes de obtener el protocolo iterador conveniente en Python 2.3, y podríamos hacer:

for line in open('filename.txt'):
    print line

He visto algunos ejemplos usando el más detallado:

with open('filename.txt') as fp:
    for line in fp:
        print line

¿Es este el método preferido para avanzar?

[editar] Entiendo que la instrucción with asegura el cierre del archivo ... pero ¿por qué no se incluye eso en el protocolo iterador para los objetos de archivo?

thebjorn
fuente
44
En mi opinión, la última sugerencia no es más detallada que la anterior. Simplemente hace más trabajo (asegura que el archivo se cierre cuando haya terminado).
azhrei
1
@azhrei es una línea más, por lo que objetivamente es más detallado.
thebjorn
77
Entiendo lo que dices, pero solo digo que comparando manzanas con manzanas, la segunda última sugerencia en tu publicación también necesita un código de manejo de excepciones para que coincida con lo que hace la última opción. Entonces, en la práctica es más detallado. Supongo que depende del contexto cuál de las dos últimas opciones es la mejor, realmente.
azhrei

Respuestas:

227

Hay exactamente una razón por la cual se prefiere lo siguiente:

with open('filename.txt') as fp:
    for line in fp:
        print line

Todos estamos mimados por el esquema de conteo de referencias relativamente determinista de CPython para la recolección de basura. Otras implementaciones hipotéticas de Python no cerrarán necesariamente el archivo "lo suficientemente rápido" sin el withbloque si usan algún otro esquema para recuperar memoria.

En tal implementación, es posible que obtenga un error de "demasiados archivos abiertos" del sistema operativo si su código abre los archivos más rápido de lo que el recolector de basura llama a los finalizadores en los controladores de archivos huérfanos. La solución habitual es activar el GC de inmediato, pero este es un truco desagradable y debe ser realizado por cada función que pueda encontrar el error, incluidas las de las bibliotecas. Qué pesadilla.

O simplemente podrías usar el withbloque.

Pregunta extra

(Deje de leer ahora si solo le interesan los aspectos objetivos de la pregunta).

¿Por qué no está incluido en el protocolo iterador para objetos de archivo?

Esta es una pregunta subjetiva sobre el diseño de API, por lo que tengo una respuesta subjetiva en dos partes.

A nivel intestinal, esto se siente mal, porque hace que el protocolo iterador haga dos cosas separadas: iterar sobre líneas y cerrar el identificador de archivo, y a menudo es una mala idea hacer que una función de aspecto simple realice dos acciones. En este caso, se siente especialmente mal porque los iteradores se relacionan de una manera cuasifuncional y basada en valores con el contenido de un archivo, pero administrar los identificadores de archivos es una tarea completamente separada. Aplastar ambos, invisiblemente, en una sola acción, es sorprendente para los humanos que leen el código y hace que sea más difícil razonar sobre el comportamiento del programa.

Otros idiomas han llegado esencialmente a la misma conclusión. Haskell coqueteó brevemente con el llamado "IO diferido" que le permite iterar sobre un archivo y cerrarlo automáticamente cuando llega al final de la transmisión, pero en la actualidad se desaconseja usar IO diferido en Haskell, y Haskell los usuarios se han movido principalmente a una administración de recursos más explícita como Conduit, que se comporta más como el withbloque en Python.

A nivel técnico, hay algunas cosas que puede hacer con un identificador de archivo en Python que no funcionarían tan bien si la iteración cerrara el identificador de archivo. Por ejemplo, supongamos que necesito iterar sobre el archivo dos veces:

with open('filename.txt') as fp:
    for line in fp:
        ...
    fp.seek(0)
    for line in fp:
        ...

Si bien este es un caso de uso menos común, considere el hecho de que podría haber agregado las tres líneas de código en la parte inferior a una base de código existente que originalmente tenía las tres líneas superiores. Si la iteración cerrara el archivo, no podría hacerlo. Por lo tanto, mantener la iteración y la administración de recursos separadas hace que sea más fácil componer fragmentos de código en un programa Python más grande y funcional.

La capacidad de composición es una de las características de usabilidad más importantes de un lenguaje o API.

Dietrich Epp
fuente
1
+1 porque explica el "cuándo" en mi comentario sobre la
operación
incluso con una implementación alternativa, el controlador with solo le causará problemas a los programas que abren cientos de archivos en una sucesión muy rápida. La mayoría de los programas pueden funcionar con la referencia del archivo colgante sin ningún problema. A menos que lo desactive, eventualmente el GC se activará en algún momento y limpiará el identificador de archivo. withsin embargo, te da tranquilidad, por lo que sigue siendo una buena práctica.
Lie Ryan
1
@DietrichEpp: quizás "referencia de archivo colgante" no eran las palabras correctas, realmente me refería a los identificadores de archivos que ya no eran accesibles pero que aún no estaban cerrados. En cualquier caso, el GC cerrará el identificador del archivo cuando recolecte el objeto del archivo, por lo tanto, siempre que no tenga referencias adicionales al objeto del archivo y no desactive el GC y no abra muchos archivos rápidamente sucesión, es poco probable que obtenga "demasiados archivos abiertos" debido a que no cierra el archivo.
Lie Ryan
1
Sí, eso es exactamente lo que quiero decir con "si su código abre archivos más rápido de lo que el recolector de basura llama a los finalizadores en los controladores de archivos huérfanos".
Dietrich Epp
1
La razón más importante para usar es que si no cierra el archivo, no necesariamente se escribirá de inmediato.
Antimonio
20

Si,

with open('filename.txt') as fp:
    for line in fp:
        print line

es el camino a seguir

No es más detallado. Es mas seguro.

eumiro
fuente
5

si la línea adicional lo apaga, puede usar una función de envoltura como esta:

def with_iter(iterable):
    with iterable as iter:
        for item in iter:
            yield item

for line in with_iter(open('...')):
    ...

en Python 3.3, la yield fromdeclaración haría esto aún más corto:

def with_iter(iterable):
    with iterable as iter:
        yield from iter
Lie Ryan
fuente
2
llamamos los xreadlines función .. y lo ponemos en un archivo llamado xreadlines.py Y estamos de vuelta a la sintaxis de Python 2.1 :-)
thebjorn
@thebjorn: tal vez, pero el ejemplo de Python 2.1 que citó no estaba a salvo del controlador de archivos no cerrados en implementaciones alternativas. Una lectura de archivo Python 2.1 que esté a salvo del controlador de archivo no cerrado tomaría al menos 5 líneas.
Lie Ryan
-1
f = open('test.txt','r')
for line in f.xreadlines():
    print line
f.close()
Rekaut
fuente
55
Esto realmente no responde la pregunta
Thayne