Python - производительность при переборе ключей словаря - PullRequest
0 голосов
/ 16 мая 2018

У меня относительно большой текстовый файл (около 7 м строк), и я хочу использовать для него специальную логику, которую я попытаюсь объяснить ниже:

A1KEY1
A2KEY1
B1KEY2
C1KEY3
D1KEY3
E1KEY4

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

Это код, который у меня есть до сих пор, но он перебирает словарь до боли и медленнее, чем больше он прогрессирует.

def filetoliststrip(file):
    file_in = str(file)
    lines = list(open(file_in, 'r'))
    content = [x.strip() for x in lines] 
    return content


dict_in = dict()    
seen = []


fileinlist = filetoliststrip(file_in)
out_file = open(file_ot, 'w')
out_file2 = open(file_ot2, 'w')
out_file3 = open(file_ot3, 'w')

counter = 0

for line in fileinlist:
    counter += 1
    keyf = line[10:69]
    print("Loading line " + str(counter) + " : " + str(line))
if keyf not in dict_in.keys():
    dict_in[keyf] = []
    dict_in[keyf].append(1)
    dict_in[keyf].append(line)
else:
    dict_in[keyf][0] += 1
    dict_in[keyf].append(line)


for j in dict_in.keys():
    print("Processing key: " + str(j))
    #print(dict_in[j])
    if dict_in[j][0] < 2:
        out_file.write(str(dict_in[j][1]))
    elif dict_in[j][0] == 2:
        for line_in in dict_in[j][1:]:
            out_file2.write(str(line_in) + "\n")
    elif dict_in[j][0] > 2:
        for line_in in dict_in[j][1:]:
            out_file3.write(str(line_in) + "\n")


out_file.close()
out_file2.close()
out_file3.close()

Я запускаю это на Windows PC i7 с 8 ГБ ОЗУ, это не должно занять несколько часов. Это проблема с тем, как я читаю файл в список? Должен ли я использовать другой метод? Заранее спасибо.

Ответы [ 3 ]

0 голосов
/ 16 мая 2018

У вас есть несколько точек, которые замедляют ваш код - нет необходимости загружать весь файл в память только для того, чтобы повторить его снова, нет необходимости получать список ключей каждый раз, когда вы хотите выполнить поиск ( if key not in dict_in: ... будет достаточно и будет невероятно быстро), вам не нужно вести подсчет строк, так как вы можете в любом случае пост-проверить длину строк ... и это лишь некоторые из них.

Я бы полностью реструктурировал ваш код как:

import collections

dict_in = collections.defaultdict(list)  # save some time with a dictionary factory
with open(file_in, "r") as f:  # open the file_in for reading
    for line in file_in:  # read the file line by line
        key = line.strip()[10:69]  # assuming this is how you get your key
        dict_in[key].append(line)  # add the line as an element of the found key
# now that we have the lines in their own key brackets, lets write them based on frequency
with open(file_ot, "w") as f1, open(file_ot2, "w") as f2, open(file_ot3, "w") as f3:
    selector = {1: f1, 2: f2}  # make our life easier with a quick length-based lookup
    for values in dict_in.values():  # use dict_in.itervalues() on Python 2.x
        selector.get(len(values), f3).writelines(values)  # write the collected lines

И вы вряд ли станете более эффективными, по крайней мере, в Python.

Имейте в виду, что это не гарантирует порядок строк в выходных данных до Python 3.7 (или CPython 3.6). Однако порядок внутри самого ключа будет сохранен. Если вам нужно сохранить порядок строк до вышеупомянутых версий Python, вам нужно будет сохранить отдельный список порядка ключей и выполнить итерацию по нему, чтобы подобрать значения dict_in по порядку.

0 голосов
/ 16 мая 2018

Первая функция:

def filetoliststrip(file):
    file_in = str(file)
    lines = list(open(file_in, 'r'))
    content = [x.strip() for x in lines] 
    return content

Здесь список необработанных строк создается только для удаления. Это потребует примерно вдвое больше памяти, чем необходимо, и, что не менее важно, несколько передач данных, которые не помещаются в кэш. Нам также не нужно постоянно делать str вещей. Таким образом, мы можем немного упростить это:

def filetoliststrip(filename):
    return [line.strip() for line in open(filename, 'r')]

Это все еще производит список. Если мы читаем данные только один раз, не сохраняя каждую строку, замените [] на (), чтобы превратить его в выражение генератора; в этом случае, поскольку строки фактически остаются нетронутыми в памяти до конца программы, мы сохраним место только для списка (который по-прежнему составляет не менее 30 МБ в вашем случае).

Тогда у нас есть основной цикл синтаксического анализа (я изменил отступ, как я думал, что это должно быть):

counter = 0

for line in fileinlist:
    counter += 1
    keyf = line[10:69]
    print("Loading line " + str(counter) + " : " + str(line))
    if keyf not in dict_in.keys():
        dict_in[keyf] = []
        dict_in[keyf].append(1)
        dict_in[keyf].append(line)
    else:
        dict_in[keyf][0] += 1
        dict_in[keyf].append(line)

Здесь есть несколько неоптимальных вещей.

Во-первых, счетчик может быть enumerate (если у вас нет повторяемого элемента, есть range или itertools.count). Изменение этого поможет с ясностью и уменьшит риск ошибок.

for counter, line in enumerate(fileinlist, 1):

Во-вторых, более эффективно формировать строку за одну операцию, чем добавлять ее из битов:

    print("Loading line {} : {}".format(counter, line))

В-третьих, нет необходимости извлекать ключи для проверки члена словаря. В Python 2 это создает новый список, который означает копирование всех ссылок, содержащихся в ключах, и становится медленнее с каждой итерацией. В Python 3 это по-прежнему означает создание объекта ключевого представления без необходимости. Просто используйте keyf not in dict_in, если проверка необходима.

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

    try:
        dictvalue = dict_in[keyf]
        dictvalue[0] += 1
        dictvalue.append(line)
    except KeyError:
        dict_in[keyf] = [1, line]

Это такой общий шаблон, однако, что у нас есть две реализации стандартной библиотеки: Counter и defaultdict. Мы могли бы использовать оба здесь, но Счетчик более практичен, когда вам нужен только счет.

from collections import defaultdict
def newentry():
    return [0]
dict_in = defaultdict(newentry)

for counter, line in enumerate(fileinlist, 1):
    keyf = line[10:69]
    print("Loading line {} : {}".format(counter, line))
    dictvalue = dict_in[keyf]
    dictvalue[0] += 1
    dictvalue.append(line)

Используя defaultdict, давайте не будем беспокоиться о том, существуют записи или нет.

Теперь мы подошли к выходной фазе. Опять же, у нас есть ненужные поиски, поэтому давайте сократим их до одной итерации:

for key, value in dict_in.iteritems():  # just items() in Python 3
    print("Processing key: " + key)
    #print(value)
    count, lines = value[0], value[1:]
    if count < 2:
        out_file.write(lines[0])
    elif count == 2:
        for line_in in lines:
            out_file2.write(line_in + "\n")
    elif count > 2:
        for line_in in lines:
            out_file3.write(line_in + "\n")

Это все еще имеет несколько неприятностей. Мы повторили написание кода, он строит другие строки (с тегами "\n"), и у него есть целый кусок аналогичного кода для каждого случая. Фактически, повторение, вероятно, вызвало ошибку: в out_file нет разделителя новой строки для единичных вхождений. Давайте рассмотрим, что действительно отличается:

for key, value in dict_in.iteritems():  # just items() in Python 3
    print("Processing key: " + key)
    #print(value)
    count, lines = value[0], value[1:]
    if count < 2:
        key_outf = out_file
    elif count == 2:
        key_outf = out_file2
    else:  #  elif count > 2:  # Test not needed
        key_outf = out_file3
    key_outf.writelines(line_in + "\n" for line_in in lines)

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

Вы заметите, что здесь есть различия между Python 2 и 3. Скорее всего, ваш код не был таким уж медленным, если его запустить в Python 3. Существует модуль совместимости под названием six для написания кода, который легче выполнять в любом из них; это позволяет вам использовать, например, six.viewkeys и six.iteritems, чтобы избежать этой ошибки.

0 голосов
/ 16 мая 2018

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

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

from collections import Counter

keys = ['A1KEY1', 'A2KEY1', 'B1KEY2', 'C1KEY3', 'D1KEY3', 'E1KEY4']
count = Counter(keys)


with open('single.txt') as f1:
    with open('double.txt') as f2:
        with open('more_than_double.txt') as f3:

        for k, v in count.items():
            if v == 1:
                f1.writelines(k)
            elif v == 2:
                f2.writelines(k)
            else:
                f3.writelines(k)
...