Finding all keys in a dictionary from a given list QUICKLY
我有一本(可能相当大)字典和一个"可能"键列表。我想快速找到字典中哪些键的值匹配。我在这里和这里发现了很多关于单个字典值的讨论,但是没有关于速度或多个条目的讨论。
我想出了四种方法,对于三种最有效的方法,我比较了它们在以下不同样本尺寸上的速度——有更好的方法吗?如果人们能提出明智的竞争者,我也会让他们接受下面的分析。
示例列表和字典创建如下:
1 2 3 4 5 6 7 | import cProfile from random import randint length = 100000 listOfRandomInts = [randint(0,length*length/10-1) for x in range(length)] dictionaryOfRandomInts = {randint(0,length*length/10-1):"It's here" for x in range(length)} |
nbsp;
方法1:
1 2 3 4 5 6 7 8 | def way1(theList,theDict): resultsList = [] for listItem in theList: if listItem in theDict: resultsList.append(theDict[listItem]) return resultsList cProfile.run('way1(listOfRandomInts,dictionaryOfRandomInts)') |
0.018秒内32次函数调用
nbsp;
方法2:错误处理:
1 2 3 4 5 6 7 8 9 10 | def way2(theList,theDict): resultsList = [] for listItem in theList: try: resultsList.append(theDict[listItem]) except: ; return resultsList cProfile.run('way2(listOfRandomInts,dictionaryOfRandomInts)') |
0.087秒内32次函数调用
nbsp;
方法3:设置交叉点:
1 2 3 4 | def way3(theList,theDict): return list(set(theList).intersection(set(theDict.keys()))) cProfile.run('way3(listOfRandomInts,dictionaryOfRandomInts)') |
0.046秒内26次函数调用
nbsp;
方法四:单纯使用
这是一个警示性的故事——这是我的第一次尝试,也是迄今为止最慢的一次!
1 2 3 4 5 6 7 8 9 | def way4(theList,theDict): resultsList = [] keys = theDict.keys() for listItem in theList: if listItem in keys: resultsList.append(theDict[listItem]) return resultsList cProfile.run('way4(listOfRandomInts,dictionaryOfRandomInts)') |
248.552秒内12次函数调用
nbsp;
编辑:将答案中给出的建议引入到我用于一致性的相同框架中。许多人注意到,在python3.x中可以获得更多的性能提升,特别是基于列表理解的方法。非常感谢您的帮助!
方法5:更好的交叉方式(感谢Jonrsharpe):
1 2 | def way5(theList, theDict): return = list(set(theList).intersection(theDict)) |
在0.037秒内调用25个函数
nbsp;
方法6:列表理解(感谢Jonrsharpe):
1 2 | def way6(theList, theDict): return [item for item in theList if item in theDict] |
0.020秒内24次函数调用
nbsp;
方法7:使用
1 2 | def way7(theList, theDict): return list(theDict.viewkeys() & theList) |
在0.026秒内调用25个函数
对于方法1-3和5-7,我使用长度为1000、10000、100000、1000000、10000000、10000000和10000000的列表/字典对它们进行了如上计时,并显示所用时间的日志图。在所有长度上,交集和语句内方法的性能都更好。梯度都在1左右(可能更高一点),表示O(N)或者稍微超线性的比例。
首先,我认为你是2.7的,所以我会用2.7做大部分的事情。但是值得注意的是,如果您真的对优化代码感兴趣,那么3.x分支将继续得到性能改进,而2.x分支永远不会得到改进。你为什么用cpython而不是pypy?
不管怎样,还需要进一步的微观优化(除了Jonrsharpe的答案中的那些:
在局部变量中缓存属性和/或全局查找(出于某种原因称为
1 2 3 4 5 6 7 8 9 10 11 12 | def way1a(theList, theDict): resultsList = [] rlappend = resultsList.append for listItem in theList: if listItem in theDict: rlappend(theDict[listItem]) return resultsList In [10]: %timeit way1(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 13.2 ms per loop In [11]: %timeit way1a(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 12.4 ms per loop |
但是对于一些特殊的操作方法,如
1 2 3 4 5 6 7 8 9 10 11 12 | def way1b(theList, theDict): resultsList = [] rlappend = resultsList.append tdin = theDict.__contains__ tdgi = theDict.__getitem__ for listItem in theList: if tdin(listItem): rlappend(tdgi(listItem)) return resultsList In [14]: %timeit way1b(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 12.8 ms per loop |
同时,jon的
1 2 3 4 5 6 7 8 9 10 11 | def way6(theList, theDict): return [theDict[item] for item in theList if item in theDict] def way6a(theList, theDict): tdin = theDict.__contains__ tdgi = theDict.__getitem__ return [tdgi(item) for item in theList if tdin(item)] In [31]: %timeit way6(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 14.7 ms per loop In [32]: %timeit way6a(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 13.9 ms per loop |
令人惊讶的是(至少对我来说),这一次确实有所帮助。不知道为什么。
但我真正要做的是:将过滤器表达式和值表达式转换为函数调用的另一个好处是我们可以使用
1 2 3 4 5 6 7 8 9 10 11 12 13 | def way6b(theList, theDict): tdin = theDict.__contains__ tdgi = theDict.__getitem__ return map(tdgi, filter(tdin, theList)) def way6c(theList, theDict): tdin = theDict.__contains__ tdgi = theDict.__getitem__ return map(tdgi, ifilter(tdin, theList)) In [34]: %timeit way6b(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 10.7 ms per loop In [35]: %timeit way6c(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 13 ms per loop |
但这一收益主要是2.x特定的;3.x的理解速度更快,而它的
您不需要将集合交集的两边都转换为集合,只需要将左侧转换为集合;右侧可以是任何可迭代的,而dict已经是其键的可迭代的。
但是,更好的是,dict的关键视图(3.x中的
1 2 3 4 5 6 7 8 9 10 11 12 13 | def way3(theList,theDict): return list(set(theList).intersection(set(theDict.keys()))) def way3a(theList,theDict): return list(set(theList).intersection(theDict)) def way3b(theList,theDict): return list(theDict.viewkeys() & theList) In [20]: %timeit way3(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 23.7 ms per loop In [20]: %timeit way3a(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 15.5 ms per loop In [20]: %timeit way3b(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 15.7 ms per loop |
最后一个没有帮助(虽然使用的是python3.4而不是2.7,但速度快了10%…),但第一个确实有帮助。
在现实生活中,您可能还想比较两个集合的大小,以决定哪个集合被设置,但这里的信息是静态的,所以编写代码来测试它是没有意义的。
不管怎样,我最快的结果是2.7版的
在我尝试过的其他几种方法中,最快的方法是简单的列表理解:
1 2 | def way6(theList, theDict): return [item for item in theList if item in theDict] |
这与最快的方法
1 2 | def way5(theList, theDict): return list(set(theList).intersection(theDict)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> import timeit >>> setup ="""from __main__ import way1, way5, way6 from random import randint length = 100000 listOfRandomInts = [randint(0,length*length/10-1) for x in range(length)] dictionaryOfRandomInts = {randint(0,length*length/10-1):"It's here" for x in range(length)} """ >>> timeit.timeit('way1(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000) 14.550477756582723 >>> timeit.timeit('way5(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000) 19.597916393388232 >>> timeit.timeit('way6(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000) 13.652289059326904 |
添加了@abarnet的建议:
1 2 | def way7(theList, theDict): return list(theDict.viewkeys() & theList) |
重新运行我现在得到的时间:
1 2 3 4 5 6 7 8 | >>> timeit.timeit('way1(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000) 13.110055883138497 >>> timeit.timeit('way5(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000) 17.292466681101036 >>> timeit.timeit('way6(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000) 14.351759544463917 >>> timeit.timeit('way7(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000) 17.206370930653392 |
1 2 3 4 | >>> timeit.timeit('way1(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000) 13.648176054011941 >>> timeit.timeit('way6(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000) 13.847062579316628 |
所以看起来set方法比list慢,但是list和list理解之间的区别(至少对我来说,令人惊讶)是有点变化的。我会说,只要挑一个,不要担心,除非它以后成为真正的瓶颈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $ ipython2 # Apple CPython 2.7.6 [snip] In [3]: %timeit way1(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 13.8 ms per loop $ python27x -m ipython # custom-built 2.7.9 [snip] In [3]: %timeit way1(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 13.7 ms per loop $ ipython3 # python.org CPython 3.4.1 [snip] In [3]: %timeit way1(listOfRandomInts, dictionaryOfRandomInts) 100 loops, best of 3: 12.8 ms per loop |
所以,只要使用一个更晚的python,速度就会提高8%。(而且在listcomp和dict-key-view版本中,加速率接近20%),这并不是因为苹果的2.7不好或者其他原因,而是因为3.x在过去5年多的时间里一直在进行优化,而2.7没有(也不会再这样做了)。
同时:
1 2 3 4 | $ ipython_pypy # PyPy 2.5.0 Python 2.7.8 [snip] In [3]: %timeit way1(listOfRandomInts, dictionaryOfRandomInts) 1000000000 loops, best of 3: 1.97 ns per loop |
只需输入5个额外的字符,就可以使速度加快70万倍。:)
我肯定这是在作弊。或者JIT隐式地将结果记忆起来,或者它注意到我甚至没有查看结果并将其推到链上,并且意识到它不需要执行任何步骤,或者做什么。但这在现实生活中有时会发生;我已经有了一大堆代码,花了3天时间调试和尝试优化,然后才意识到它所做的一切都是不必要的……
无论如何,从Pypy的角度来看,10倍的加速是相当典型的,即使它不能作弊。这比调整属性查找或者颠倒谁将变成5%的集合要容易得多。
Jython更不可预测,有时速度几乎和Pypy一样快,有时比CPython慢得多。不幸的是,jython 2.5.3中的