Python - 迭代字典键时的性能
Python - Performance when iterating over dictionary keys
我有一个相对较大的文本文件(大约 700 万行),我想 运行 它的特定逻辑,我将在下面尝试解释:
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 上执行此操作,执行此操作应该不会花费数小时。这是我将文件读入列表的方式的问题吗?我应该使用不同的方法吗?提前致谢。
您一次将一个非常大的文件加载到内存中。当你实际上不需要这些线,而你只需要处理它时,使用 generator。它更节省内存。
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)
您有多个点会减慢您的代码速度 - 无需将整个文件加载到内存中只是为了再次对其进行迭代,无需每次都获取键列表查找(if key not in dict_in: ...
就足够了,而且速度非常快),您不需要保留行数,因为您可以 post-检查行的长度...仅举几例。
我会将您的代码完全重构为:
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
值。
第一个函数:
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')]
这仍然会生成一个列表。如果我们只读取一次数据,而不是存储每一行,请将 []
替换为 ()
以将其转换为生成器表达式;在这种情况下,由于行实际上在程序结束之前都完整地保留在内存中,因此我们只会为列表保存 space (在您的情况下仍然至少有 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
(如果没有可迭代对象,则有 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
。我们可以在这里同时使用两者,但是当您只需要计数时,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)
我离开了换行连接,因为将它们混合为单独的调用更加复杂。该字符串是短暂的,它用于将换行符放在同一位置的目的:它使得在 OS 级别不太可能一行被并发写入打断。
您会注意到这里有 Python 2 和 3 个不同之处。如果 Python 3 中的 运行 首先,您的代码很可能并没有那么慢。存在一个名为 six 的兼容性模块,可以编写更容易 运行 的代码;它可以让你使用例如six.viewkeys
和 six.iteritems
来避免这个陷阱。
我有一个相对较大的文本文件(大约 700 万行),我想 运行 它的特定逻辑,我将在下面尝试解释:
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 上执行此操作,执行此操作应该不会花费数小时。这是我将文件读入列表的方式的问题吗?我应该使用不同的方法吗?提前致谢。
您一次将一个非常大的文件加载到内存中。当你实际上不需要这些线,而你只需要处理它时,使用 generator。它更节省内存。
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)
您有多个点会减慢您的代码速度 - 无需将整个文件加载到内存中只是为了再次对其进行迭代,无需每次都获取键列表查找(if key not in dict_in: ...
就足够了,而且速度非常快),您不需要保留行数,因为您可以 post-检查行的长度...仅举几例。
我会将您的代码完全重构为:
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
值。
第一个函数:
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')]
这仍然会生成一个列表。如果我们只读取一次数据,而不是存储每一行,请将 []
替换为 ()
以将其转换为生成器表达式;在这种情况下,由于行实际上在程序结束之前都完整地保留在内存中,因此我们只会为列表保存 space (在您的情况下仍然至少有 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
(如果没有可迭代对象,则有 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
。我们可以在这里同时使用两者,但是当您只需要计数时,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)
我离开了换行连接,因为将它们混合为单独的调用更加复杂。该字符串是短暂的,它用于将换行符放在同一位置的目的:它使得在 OS 级别不太可能一行被并发写入打断。
您会注意到这里有 Python 2 和 3 个不同之处。如果 Python 3 中的 运行 首先,您的代码很可能并没有那么慢。存在一个名为 six 的兼容性模块,可以编写更容易 运行 的代码;它可以让你使用例如six.viewkeys
和 six.iteritems
来避免这个陷阱。