Разница между itertools.islice и функцией чтения

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

Для практики я создал файл с ~6,5 миллионами строк и написал пару функций, чтобы разбить его на ~7 файлов по миллиону строк в каждом. В первой функции я использовал метод чтения файла python для создания логики, при которой новый файл создается после чтения 1 миллиона строк, пока мы не найдем последний файл, в котором записаны оставшиеся 500 тысяч строк.

Эта функция работает НАВСЕГДА.

Затем я создал еще одну функцию для разделения файла с помощью itertools.islice. Это заняло чуть менее ~ 2 секунд.

Теперь я знаю, что islice - это итератор, который выполняет итерацию файлового объекта, поэтому ожидается, что он будет более эффективным с точки зрения использования памяти. Однако чем он отличается от метода read()?

Я думал, что даже read() проходит каждую строку в файле одну за другой (что-то вроде итератора..). Итак, я ожидал, что производительность двух программ будет одинаковой. Ребята, помогите мне понять, почему islice НАМНОГО быстрее?

Вот оба фрагмента кода -

1 с использованием чтения () -

with open("bigfile.txt","r") as f:
    filenum = 1
    j = 1
    for i, line in enumerate(f):
        if j <= 1000000:
            with open("big_out_%d" % filenum, "a") as outfile:
                outfile.write(line)
        j += 1
        if j == 1000000:
            j = 1
            filenum += 1
            with open("big_out_%d" % filenum, "a") as outfile:
                outfile.write(line)

2 с использованием islice -

import itertools
import time

start = time.time()

with open("bigfile.txt","r") as f:
    i = 1
    while True:
        chunk = list(itertools.islice(f, 1000000))
        if not chunk:
            print "reached the end"
            break
        with open("out%d.txt" % i, "w") as out:
            out.writelines(chunk)
        print i
        i += 1

end = time.time()
print "time is %d" % ((end-start))

person user168115    schedule 19.05.2019    source источник
comment
В вашем первом примере отсутствует read... Чтение 1M строк по одной или сразу имеет большое значение. Ваш первый выполняет чтение-запись-чтение-запись каждой строки, максимизируя накладные расходы ввода-вывода.   -  person Andras Deak    schedule 19.05.2019
comment
Вы немедленно преобразуете объект islice в список, поэтому этот код не выигрывает от того факта, что islice не будет читать все строки сразу. Оказывается, чтение большого фрагмента файла за один раз, а затем запись этого фрагмента одновременно выполняется быстрее, чем чтение и запись построчно.   -  person ForceBru    schedule 19.05.2019
comment
@ForceBru помогает, потому что позволяет лениво читать первые 1 миллион строк. Конечно, можно было бы использовать listcomp.   -  person Andras Deak    schedule 19.05.2019
comment
@AndrasDeak, но объект islice немедленно преобразуется в список, поэтому все 1M строк будут прочитаны сразу, а не лениво, потому что списки не ленивы.   -  person ForceBru    schedule 19.05.2019
comment
Лениво по сравнению с f.read() или list(f), @ForceBru.   -  person Andras Deak    schedule 19.05.2019
comment
@ForceBru - Причина, по которой мне пришлось преобразовать объект islice в список, заключалась в том, что без этого мой код застревал в бесконечном цикле. Вот что у меня было - ``` chunk = itertools.islice(f, 1000000) if not chunk: print достиг конца break ``` Проблема с этим кодом заключалась в том, что если бы не chunk, он никогда не совпадал, так как даже после всех строк моего файл был использован, объект islice существовал... Есть ли другой подход для этого? Я хотел бы сохранить логику генератора islice нетронутой и использовать ее для извлечения каждой строки и записи ее в файл.   -  person user168115    schedule 19.05.2019


Ответы (2)


Разница не имеет ничего общего с islice и read(). Ваши две программы сильно различаются по своей логике.

В первом листинге вы перебираете строки файла. В каждой итерации цикла вы открываете файл, пишете одну строку, а затем снова закрываете файл. (Синтаксис with open заставляет файл закрываться в конце блока with:). К тому времени, когда вы закончите, вы запишете 6500000 строк в семь разных файлов, но вы также откроете и закроете 6500000 файлов. Я не удивлен, что ОС не может сделать это эффективно.

Во втором листинге вы читаете кусок из 1000000 строк, а затем пишете все это один раз. Вы по-прежнему пишете 6500000 строк, но здесь вы выполняете 7 открытий и 7 закрытий. Совсем не то же самое.

Использование with: для выходных файлов делает ваш первый листинг очень неуклюжим. Попробуй это:

with open("bigfile.txt","r") as f:
    filenum = 1
    j = 1
    outfile = open("big_out_%d" % filenum, "w")
    try:
        for line in f:
            outfile.write(line)
            j += 1
            if j == 1000000:
                outfile.close()
                j = 1
                filenum += 1
                outfile = open("big_out_%d" % filenum, "w")
    finally:
        outfile.close()

Я не тестировал этот код. Если в нем есть ошибка, ее должно быть легко исправить.

При таком подходе вы никогда не загружаете в память более одной строки за раз.

person Paul Cornelius    schedule 19.05.2019
comment
У меня есть вопрос по этому поводу. При таком подходе вы никогда не загружаете в память более одной строки за раз. Поскольку я пишу каждую строку отдельно в выходной файл, в котором хранится 1 миллион записей, память действительно загружает 1 миллион строк. Нет? Я понимаю, что есть явная экономия от необходимости открывать/закрывать файл 7 раз по сравнению с 6,5 млн раз, но есть ли какая-либо польза от загрузки каждой строки и записи ее в файл по сравнению с загрузкой 1 млн строк с использованием islice в список и затем запись списка в файл. Я думаю, что в последнем подходе я попал в точку, где в памяти есть 2 миллиона строк? (1M в списке и файле?) - person user168115; 19.05.2019
comment
Под памятью я подразумеваю память программы или, может быть, вы могли бы назвать ее памятью Python. Во второй программе ваша переменная chunk представляет собой список, содержащий один миллион строк, и все они должны одновременно находиться в памяти программы. Как только вы назначите новый список переменной chunk, Python восстановит память, используемую для хранения предыдущего списка. У вас, как вы говорите, на мгновение будет 2 миллиона строк в памяти. Но в моей программе переменная line — это всего лишь одна строка, так что вместо 2 миллионов живых объектов у меня всего 2. Конечно, для современного ПК два миллиона — это немного. - person Paul Cornelius; 20.05.2019

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

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

with open("bigfile.txt","r") as f:
    outfile = None
    for i, line in enumerate(f):
        if i % 1000000 == 0:
            if outfile:
                outfile.close()
            outfile = open("big_out_%d" % (i // 1000000), "w")
        outfile.write(line)
    if outfile:
        outfile.close()

Обратите внимание, что я немного упростил ситуацию, используя индекс i из enumerate для всего подсчета, а не вручную обновляя целые числа filenum или j в вашем первом коде. Маловероятно, что это окажет значительное влияние на производительность, но сделает код намного приятнее.

person Blckknght    schedule 19.05.2019