Подтвердить что ты не робот

Python readlines() использование и эффективная практика для чтения

У меня есть проблема, чтобы разобрать 1000 текстовых файлов (около 3000 строк в каждом файле размером ~ 400 КБ) в папке. Я прочитал их, используя 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  

Это работает отлично для образца с моих входов (50 100 файлов). Когда я запускал весь ввод более чем 5K файлов, время было нигде близко к линейному приращению. Я планировал провести анализ производительности и проанализировать Cprofile. Время, затрачиваемое на большее количество файлов, экспоненциально возрастает с увеличением ставок при входе в файлы 7K.

Вот кумулятивное время, затраченное на чтение строк, сначала → 354 файла (образец со входа) и второй → 7473 файла (всего ввода)

 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}

Из-за этого время, затраченное моим кодом, не линейно масштабируется по мере увеличения ввода. Я прочитал некоторые примечания к doc на readlines(), где люди утверждали, что этот readlines() читает весь файл в памяти и, следовательно, обычно потребляет больше памяти по сравнению с readline() или read().

Я согласен с этим моментом, но если сборщик мусора автоматически очистит загруженный контент из памяти в конце моего цикла, значит, в любой момент моя память должна иметь только содержимое моего текущего обработанного файла? Но здесь есть какая-то уловка. Может кто-нибудь дать некоторое понимание этой проблемы.

Является ли это неотъемлемым поведением readlines() или неправильной интерпретации сборщика мусора python. Рад знать.

Кроме того, предложите несколько альтернативных способов сделать то же самое в памяти и эффективно. ТИА.

4b9b3361

Ответ 1

Короткий вариант: Эффективный способ использования readlines() - не использовать его. Когда-либо.


Я прочитал некоторые примечания к doc на readlines(), где люди утверждали, что этот readlines() читает весь файл в памяти и, следовательно, обычно потребляет больше памяти по сравнению с readline() или read().

Документация для readlines() явно гарантирует, что она считывает весь файл в память и анализирует его в строках и создает list полный str из этих строк.

Но документация для read() также гарантирует, что она считывает весь файл в память и создает str ing, так что не помогает.


Помимо использования большего количества памяти, это также означает, что вы не можете выполнять какую-либо работу, пока все это не будет прочитано. Если вы будете чередоваться с чтением и обработкой даже самым наивным образом, вы получите хотя бы некоторую конвейерную поддержку (благодаря дисковым кэшам ОС, DMA, конвейерам CPU и т.д.), Поэтому вы будете работать над одной партией, а следующая партия читается. Но если вы вынудите компьютер прочитать весь файл, а затем проанализируйте весь файл, а затем запустите свой код, вы получите только одну область перекрывающейся работы для всего файла, а не одну область перекрывающейся работы за чтение.


Вы можете обойти это тремя способами:

  • Напишите цикл вокруг readlines(sizehint), read(size) или readline().
  • Просто используйте файл как ленивый итератор, не вызывая никаких из них.
  • mmap файл, который позволяет вам рассматривать его как гигантскую строку без предварительного ее чтения.

Например, это должно сразу прочитать все foo:

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

Но это только чтение примерно 8K за раз:

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

И это только чтение по одной строке за раз, хотя Python разрешено (и будет) выбирать хороший размер буфера, чтобы ускорить выполнение.

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

И это будет делать то же самое, что и предыдущее:

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

Тем:

но если сборщик мусора автоматически очистит загруженный контент из памяти в конце моего цикла, значит, в любой момент моя память должна иметь только содержимое моего текущего обработанного файла?

Python не дает никаких гарантий относительно сбора мусора.

В реализации CPython используется пересчет для GC, что означает, что в вашем коде, как только file_content будет отскакивать или уходить, гигантский список строк и все строки внутри него будут освобождены до freelist, означающий, что одна и та же память может быть повторно использована для вашего следующего прохода.

Однако все эти распределения, копии и освобождения не являются бесплатными - гораздо быстрее не выполнять их, чем выполнять их.

Кроме того, ваши строки, разбросанные по большому объему памяти, вместо повторного использования одного и того же маленького фрагмента памяти снова и снова вредят вашему поведению кэша.

Плюс, в то время как использование памяти может быть постоянным (или, скорее, линейным размером вашего самого большого файла, а не суммой размеров вашего файла), этот прилив malloc для его расширения в первый раз быть одной из самых медленных вещей, которые вы делаете (что также значительно усложняет выполнение сравнений производительности).


Объединяя все вместе, вот как я напишу вашу программу:

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 ...  

Или, может быть:

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 ...

Ответ 2

Прочитайте строку за строкой, а не весь файл:

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

Еще лучше использовать with для автоматического закрытия файла:

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

Вышеописанное будет читать объект файла с использованием итератора, по одной строке за раз.