Python - 迭代字典键时的性能

时间:2018-05-16 06:54:51

标签: python loops dictionary frequency large-files

我有一个相对较大的文本文件(大约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()

我在带有8GB Ram的Windows PC i7上运行它,这应该不需要花费数小时才能执行。这是我将文件读入列表的方式的问题吗?我应该使用不同的方法吗?提前谢谢。

3 个答案:

答案 0 :(得分:3)

你有多个点会降低你的代码速度 - 没有必要将整个文件加载到内存中只是为了再次迭代它,每次你想要查找时都不需要获取一个键列表( 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值。

答案 1 :(得分:1)

第一个功能:

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')]

这仍然会产生一个列表。如果我们只读取数据一次,而不是存储每一行​​,请将[]替换为(),将其转换为生成器表达式;在这种情况下,由于行在程序结束之前实际上在内存中保持完整,我们只保存列表的空间(在您的情况下仍然至少为30MB)。

然后我们有了主要的解析循环(我按照我的想法调整了缩进):

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(当您没有可迭代时,有rangeitertools.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]

这是一种常见模式,但我们有两个标准库实现:Counterdefaultdict。我们可以在这里使用,但是当你只想要计数时,Counter更实用。

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.viewkeyssix.iteritems以避免此问题。

答案 2 :(得分:0)

您可以立即将非常大的文件加载到内存中。如果您实际上不需要线路,并且只需要处理它,请使用生成器。它的内存效率更高。

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)