关于python:快速从给定列表中查找字典中的所有键

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:'in'关键字:

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;

方法四:单纯使用dict.keys()

这是一个警示性的故事——这是我的第一次尝试,也是迄今为止最慢的一次!

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:使用&关键字(谢谢jornsharpe):

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)或者稍微超线性的比例。

Log-Log plot comparing time-scaling of the 6 sensible methods with list/dict length


首先,我认为你是2.7的,所以我会用2.7做大部分的事情。但是值得注意的是,如果您真的对优化代码感兴趣,那么3.x分支将继续得到性能改进,而2.x分支永远不会得到改进。你为什么用cpython而不是pypy?

不管怎样,还需要进一步的微观优化(除了Jonrsharpe的答案中的那些:

在局部变量中缓存属性和/或全局查找(出于某种原因称为LOAD_FAST)。例如:

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

但是对于一些特殊的操作方法,如__contains____getitem__来说,这可能是不值得的。当然,除非你尝试,否则你不会知道:

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的way6答案已经通过使用listcomp完全优化了resultList.append,我们只是看到优化他所做的查找可能不会有帮助。尤其是在3.x中,理解将被编译成它自己的函数,但即使在2.7中,我也不会期望有任何好处,因为与显式循环中的原因相同。但我们还是要确保:

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

令人惊讶的是(至少对我来说),这一次确实有所帮助。不知道为什么。

但我真正要做的是:将过滤器表达式和值表达式转换为函数调用的另一个好处是我们可以使用filtermap

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的理解速度更快,而它的list(map(filter(…)))比2.x的map(filter(…))map(ifilter(…))慢。

您不需要将集合交集的两边都转换为集合,只需要将左侧转换为集合;右侧可以是任何可迭代的,而dict已经是其键的可迭代的。

但是,更好的是,dict的关键视图(3.x中的dict.keys,2.7中的dict.keyview)已经是一个集合状的对象,并且由dict的哈希表支持,因此您不需要转换任何内容。(它没有完全相同的接口,它没有intersection方法,但它的&运算符采用iterables,不像set方法采用iterables,后者的intersection方法采用iterables,但它的&只采用sets。这很烦人,但我们只关心这里的表现,对吗?)

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版的map(filter(…)),有相当大的优势。在3.4版本中(我在这里没有显示),jon的listcomp速度最快(甚至固定为返回值而不是键),比任何2.7方法都快。另外,3.4最快的设置操作(使用键视图作为一个集合,列表作为一个可迭代的)比2.7更接近于迭代方法。


在我尝试过的其他几种方法中,最快的方法是简单的列表理解:

1
2
def way6(theList, theDict):
    return [item for item in theList if item in theDict]

这与最快的方法way1运行相同的过程,但速度更快。相比之下,最快的基于set的方法是

1
2
def way5(theList, theDict):
    return list(set(theList).intersection(theDict))

timeit结果:

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

way1way6已经换了位置,所以我重新运行:

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中的timeit被破坏了,我刚刚通过从rc2升级到rc3完全破坏了jython 2.7,所以……今天没有测试。类似地,Ironpython基本上是Jython在不同的虚拟机上重做的;它通常更快,但又不可预知。但是我当前版本的mono和我当前版本的ironpython不能很好地结合在一起,所以也没有测试。