Python readlines () uso y práctica eficiente para leer

Tengo un problema para analizar miles de archivos de texto (alrededor de 3000 líneas en cada archivo de tamaño de ~ 400 KB) en una carpeta. Los leí usando readlines,

for filename in os.listdir (input_dir) : if filename.endswith(".gz"): f = gzip.open(file, 'rb') else: f = open(file, 'rb') file_content = f.readlines() f.close() len_file = len(file_content) while i < len_file: line = file_content[i].split(delimiter) ... my logic ... i += 1 

Esto funciona completamente bien para la muestra de mis entradas (50,100 archivos). Cuando ejecuté en toda la entrada más de 5K archivos, el tiempo empleado no se acercó al incremento lineal. Planeé hacer un análisis de rendimiento e hice un análisis de perfil de C. El tiempo que se tarda en boost la cantidad de archivos aumenta exponencialmente al alcanzar tasas más bajas cuando las entradas llegan a los archivos de 7K.

Aquí está el tiempo acumulado para las líneas de lectura, primero -> 354 archivos (muestra de entrada) y segundo -> 7473 archivos (entrada completa)

  ncalls tottime percall cumtime percall filename:lineno(function) 354 0.192 0.001 **0.192** 0.001 {method 'readlines' of 'file' objects} 7473 1329.380 0.178 **1329.380** 0.178 {method 'readlines' of 'file' objects} 

Debido a esto, el tiempo que tarda mi código no se escala linealmente a medida que aumenta la entrada. Leí algunas notas de documentos en readlines() , donde las personas afirman que este readlines() lee todo el contenido del archivo en la memoria y, por lo tanto, generalmente consume más memoria en comparación con readline() o read() .

Estoy de acuerdo con este punto, pero ¿debería el recolector de basura borrar automáticamente el contenido cargado de la memoria al final de mi bucle, por lo tanto, en cualquier momento, mi memoria debería tener solo el contenido de mi archivo procesado actualmente? Pero, hay algo de captura aquí. ¿Alguien puede dar algunas ideas sobre este tema.

Es este un comportamiento inherente de readlines() o mi interpretación errónea del recolector de basura Python. Contento de saber.

Además, sugiera algunas formas alternativas de hacer lo mismo en la memoria y de manera eficiente en el tiempo. TIA.

La versión corta es: La forma eficiente de usar readlines() es no usarla. Siempre.


Leí algunas notas de documentos en readlines() , donde las personas afirman que este readlines() lee todo el contenido del archivo en la memoria y, por lo tanto, generalmente consume más memoria en comparación con readline () o read ().

La documentación para readlines() garantiza explícitamente que lee todo el archivo en la memoria, lo analiza en líneas y crea una list completa de cadenas fuera de esas líneas.

Pero la documentación para read() también garantiza que lee todo el archivo en la memoria, y construye una str , por lo que no ayuda.


Además de usar más memoria, esto también significa que no puede hacer ningún trabajo hasta que se lea todo. Si alterna la lectura y el procesamiento incluso de la forma más ingenua, se beneficiará de al menos algo de canalización (gracias a la caché de disco del SO, DMA, CPU de la tubería, etc.), por lo que estará trabajando en un lote mientras que el siguiente lote esta siendo leido Pero si obliga a la computadora a leer todo el archivo, luego lo analiza, luego ejecuta su código, solo obtiene una región de trabajo superpuesto para todo el archivo, en lugar de una región de trabajo superpuesto por lectura.


Puedes solucionar esto de tres maneras:

  1. Escriba un bucle alrededor de readlines(sizehint) read(size) readlines(sizehint) , read(size) o readline() .
  2. Simplemente use el archivo como un iterador perezoso sin llamar a ninguno de estos.
  3. mmap el archivo, que le permite tratarlo como una cadena gigante sin leerlo primero.

Por ejemplo, esto tiene que leer todo foo a la vez:

 with open('foo') as f: lines = f.readlines() for line in lines: pass 

Pero esto solo lee alrededor de 8K a la vez:

 with open('foo') as f: while True: lines = f.readlines(8192) if not lines: break for line in lines: pass 

Y esto solo lee una línea a la vez, aunque Python tiene permitido (y lo hará) elegir un buen tamaño de búfer para hacer las cosas más rápido.

 with open('foo') as f: while True: line = f.readline() if not line: break pass 

Y esto hará exactamente lo mismo que el anterior:

 with open('foo') as f: for line in f: pass 

Mientras tanto:

pero ¿debería el recolector de basura borrar automáticamente el contenido cargado de la memoria al final de mi ciclo, por lo tanto, en cualquier momento, mi memoria debería tener solo el contenido de mi archivo procesado actualmente?

Python no ofrece tales garantías sobre la recolección de basura.

La implementación de CPython pasa a usar refcounting para GC, lo que significa que en su código, tan pronto como file_content se file_content o desaparezca, la lista gigante de cadenas, y todas las cadenas que contiene, se liberarán a la lista gratuita, lo que significa que La misma memoria puede ser reutilizada nuevamente para su próximo pase.

Sin embargo, todas esas asignaciones, copias y desasignaciones no son gratis, es mucho más rápido no hacerlas que hacerlo.

Además de eso, tener sus cadenas dispersas en una gran franja de memoria en lugar de reutilizar el mismo trozo pequeño de memoria una y otra vez afecta su comportamiento de caché.

Además, si bien el uso de la memoria puede ser constante (o, más bien, lineal en el tamaño de su archivo más grande, en lugar de en la sum de los tamaños de sus archivos), la aceleración de malloc s para expandirlo la primera vez será una de las lo más lento que haces (lo que también hace que sea mucho más difícil hacer comparaciones de rendimiento).


Juntándolo todo, así es como escribiría tu progtwig:

 for filename in os.listdir(input_dir): with open(filename, 'rb') as f: if filename.endswith(".gz"): f = gzip.open(fileobj=f) words = (line.split(delimiter) for line in f) ... my logic ... 

O tal vez:

 for filename in os.listdir(input_dir): if filename.endswith(".gz"): f = gzip.open(filename, 'rb') else: f = open(filename, 'rb') with contextlib.closing(f): words = (line.split(delimiter) for line in f) ... my logic ... 

Lea línea por línea, no todo el archivo:

 for line in open(file_name, 'rb'): # process line here 

Aún mejor uso with para cerrar automáticamente el archivo:

 with open(file_name, 'rb') as f: for line in f: # process line here 

Lo anterior leerá el objeto de archivo utilizando un iterador, una línea a la vez.