Python 字典搜寻及;插入优化
当使用字典时,我遇到了严重的速度减慢,因为字典会增加到几千个键 我正在处理一个包含1000000行数据的文件,我正在使用字典构建一个类似于图形的数据结构 这是我的瓶颈函数Python 字典搜寻及;插入优化,python,Python,当使用字典时,我遇到了严重的速度减慢,因为字典会增加到几千个键 我正在处理一个包含1000000行数据的文件,我正在使用字典构建一个类似于图形的数据结构 这是我的瓶颈函数 def create_edge(node_a, node_b, graph): if node_a not in graph.keys(): graph[node_a] = {node_b: 1} elif node_b in graph[node_a].keys(): grap
def create_edge(node_a, node_b, graph):
if node_a not in graph.keys():
graph[node_a] = {node_b: 1}
elif node_b in graph[node_a].keys():
graph[node_a][node_b] += 1
else:
graph[node_a][node_b] = 1
create_edge
将从node_a
到node_b
创建和设置边,或者将它们之间已经存在的边的权重加1
由于我的节点是由一个字符串唯一id标识的,所以我使用字典来存储,假设如果存在一个键,则搜索和插入平均需要O(1)
如果我把create_edge
注释掉,我可以每秒处理大约20000条记录,而create_edge
作为我的管道的一部分,大约每秒处理20条记录
前100条记录的处理大约需要500毫秒。
当字典大小增加到10000左右时—处理100条记录大约需要15000ms,每个记录进程平均调用create\u edge
大约4次—因此当字典大小为10000时,400次调用create\u edge
需要15秒
首先,这些运行时有意义吗?对我来说似乎很重要,如果我错了就纠正我
第二,为更好地运行时间而优化词典使用的建议将不胜感激
我希望字典大小至少为100000,以完成整个1000000条记录的处理
编辑:结论 你在钱上是对的,在这里犯了两个noob错误:)
keys()
调用极大地增加了复杂性,将每次边插入的时间从常量时间变为多边形时间(平方),将图中的if节点替换为图中的if节点。keys()
将在~300ms内生成100条记录的常量处理时间
第二个错误是virtualenv配置,这让我相信我在使用python3,而实际上我在使用python2
python3do将keys()代码优化为一个常量时间搜索,这对运行时有好处,但对正确的代码样式没有好处
非常感谢你的帮助。
在删除键()
调用后,我执行了一次运行时比较
# graph = {}
python version: 3.6.3
start time 11:44:56
Number of records: 1029493
graph created, 1231630 nodes
end time 11:50:35
total ~05:39
# graph = defaultdict(lambda : defaultdict(int))
python version: 3.6.3
start time 11:54:52
Number of records: 1029493
graph created, 1231630 nodes
end time 12:00:34
total ~05:42
# graph = {}
python version: 2.7.10
start time 12:03:25
Number of records: 1029493
graph created, 1231630 nodes
end time 12:09:40
total ~06:15
当测试dict
中是否存在密钥时,只需使用key in d
,而不是key in d.keys()
。提取密钥以测试成员资格首先否定了使用dict
的好处
请尝试以下操作:
def create_edge(node_a, node_b, graph):
if node_a not in graph:
graph[node_a] = {node_b: 1}
elif node_b in graph[node_a]:
graph[node_a][node_b] += 1
else:
graph[node_a][node_b] = 1
请注意,根本不调用keys()
。这应该比你现在做的要快得多
请注意,在Python2中,keys()
检查将比Python3慢得多,因为在Python2中,keys()
创建了一个完整密钥集的列表。它在Python3中的工作方式不同,但即使在Python3中,不使用keys()
,直接检查成员身份也会更快。您不能只使用带有defaultdict(int)的defaultdict,如下所示:
返回:
defaultdict(<function __main__.<lambda>>,
{'a': defaultdict(int, {'b': 2, 'c': 1})})
# equal to: {'a': {'b': 2, 'c': 1}}
defaultdict(,
{'a':defaultdict(int,{'b':2,'c':1})
#等于:{'a':{'b':2,'c':1}
我尝试了几种方法,这似乎是一种有效的方法。此方法首先使用计数器统计所有事件,然后构建字典。感谢@Stefan Pochmann提供基准脚本。我用的一个是从
我正在使用Python3.6,结果在我的计算机上进行了测试
from timeit import timeit
from collections import defaultdict, Counter
from random import shuffle
from itertools import product
def f(): # OP's method modified with Tom Karzes' answer above.
d = {}
for i, j in edges:
if i not in d:
d[i] = {j: 1}
elif j in d[i]:
d[i][j] += 1
else:
d[i][j] = 1
def count_first():
d = dict()
for (v, w), c in Counter(edges).items():
if v not in d:
d[v] = {w: c}
else:
d[v][w] = c
# Alternatively, (Thanks to Jean-François Fabre to point it out.)
# d = defaultdict(lambda : defaultdict(int))
# for (v, w), c in Counter(edges).items():
# d[v][w] = c
edges = list(product(range(300), repeat=2)) * 10
shuffle(edges)
# %timeit f()
270 ms ± 23.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# %timeit count_first()
180 ms ± 15.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
声明人:然而,我从ideone.com上得到的count\u first()
的结果比OP的答案f()
要慢
Stefan Pochmann做了一个基准测试来比较Python2和Python3中的不同方法。可以找到他在Python 2中的结果。对于Python3,请选中。感谢他并感谢他的代码审查。这是什么Python版本?您是否有机会使用Python 2?因为.keys()
获取的是键列表,而不是字典。因此,在create_edge
中,您将执行两次O(n)
操作,从转换为列表的整个词典中搜索两次。如果图中的节点a
,则需要执行,请参阅。注意,如下面的注释所述,这仅适用于使用Python 2,而不是3。@SpencerWieczorek这是我怀疑的,因为它是一个著名的Python反模式,但在Python 3中,它返回一个带有O(1)成员身份测试的键视图。@AlanSTACK不,它不返回迭代器,尽管dict_keys
对象是可编辑的。不相信我?尝试next({}.keys())
关于keys()部分的解释很好,但是如果没有defaultdict/Counter,它仍然会很慢。@Jean-Françoisfare Adefaultdict
会更快,但代码中最大的问题是它使用的是keys()
,特别是在OP使用Python 2的情况下。使用defaultdict
最多可以避免冗余的密钥检查,从而将速度提高三倍,但它的优点是它可以消除错误调用keys()
的可能性。@Jean-Françoisfare请出示支持该声明的基准。@Jean-Françoisfare Mine不同意,defaultdict+计数器占用的时间几乎是OP(固定)方式的两倍:@StefanPochmann有趣。我猜defaultdict
的效率比我想象的要低。这实际上比OP快吗(当然是在修复了他们的.keys()
错误之后)。你能分享你的基准测试结果吗?因为我不是坐在数据集上,我想OP可以做到。@StefanPochmann它可能会快得多。上下文切换(调用create\u edge
)非常昂贵。@PatrickHaugh我看不出有什么大的区别,看看下面的时间:。这就是用这个函数添加边。如果我是内联的,OP的方式在我的测试中要快得多:我一定会尝试使用lambdas和defaultdict并发布结果,谢谢!我的速度稍微快一点
from timeit import timeit
from collections import defaultdict, Counter
from random import shuffle
from itertools import product
def f(): # OP's method modified with Tom Karzes' answer above.
d = {}
for i, j in edges:
if i not in d:
d[i] = {j: 1}
elif j in d[i]:
d[i][j] += 1
else:
d[i][j] = 1
def count_first():
d = dict()
for (v, w), c in Counter(edges).items():
if v not in d:
d[v] = {w: c}
else:
d[v][w] = c
# Alternatively, (Thanks to Jean-François Fabre to point it out.)
# d = defaultdict(lambda : defaultdict(int))
# for (v, w), c in Counter(edges).items():
# d[v][w] = c
edges = list(product(range(300), repeat=2)) * 10
shuffle(edges)
# %timeit f()
270 ms ± 23.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# %timeit count_first()
180 ms ± 15.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)