【问题标题】:Finding all keys in a dictionary from a given list QUICKLY快速从给定列表中查找字典中的所有键
【发布时间】:2015-04-30 10:29:25
【问题描述】:

我有一个(可能相当大的)字典和一个“可能”键的列表。我想快速找到字典中哪些键具有匹配值。我发现很多关于单个字典值herehere 的讨论,但没有讨论速度或多个条目。

我提出了四种方法,对于效果最好的三种方法,我在下面比较了它们在不同样本量上的速度 - 有更好的方法吗?如果人们可以提出明智的竞争者,我也会让他们接受下面的分析。

示例列表和字典的创建如下:

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)}

 

方法一:'in'关键字:

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 个函数

 

方法二:错误处理:

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 个函数

 

方法三:设置交集:

def way3(theList,theDict):
    return list(set(theList).intersection(set(theDict.keys())))

cProfile.run('way3(listOfRandomInts,dictionaryOfRandomInts)')

0.046 秒内调用 26 个函数

 

方法四:幼稚使用dict.keys()

这是一个警示故事 - 这是我的第一次尝试,到目前为止是最慢的!

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

12 次函数调用在 248.552 秒内

 

编辑:将答案中给出的建议纳入我为保持一致性而使用的同一框架中。许多人已经注意到在 Python 3.x 中可以获得更多的性能提升,尤其是基于列表理解的方法。非常感谢所有的帮助!

方法 5:更好的交叉路口方式(感谢 jonrsharpe):

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

25 次函数调用在 0.037 秒内

 

方法 6:列表理解(感谢 jonrsharpe):

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

24 次函数调用在 0.020 秒内

 

方法 7:使用 & 关键字(感谢 jonrsharpe):

def way7(theList, theDict):
    return list(theDict.viewkeys() & theList)

25 次函数调用在 0.026 秒内

对于方法 1-3 和 5-7,我用长度为 1000、10000、100000、1000000、10000000 和 100000000 的列表/字典对它们进行了定时,并显示了所用时间的对数图。在所有长度上,交集和语句内方法的性能更好。梯度都在 1 左右(可能更高一点),表示 O(n) 或者可能稍微超线性缩放。

【问题讨论】:

  • Python 的哪个版本?在 2.x 中,dict.keys 返回一个列表,例如,我希望set(theDict.keys())set(theDict) 慢,而set(theList).intersection(theDict)set(theList).intersection(set(theDict.keys())) 快(并且timeit 同意.. .)
  • 此外,list comprehension 通常比循环和appending 更快。
  • 为了将来参考,[c]profile 不适合做基准测试,尤其是微基准测试。事实上,the docs 顶部有一个注释说明了这一点,并建议使用timeit(就像所有答案一样)。在这种特殊情况下,除了 timeit 没有做的所有其他事情之外,它还增加了每个 Python 函数调用的开销,但不会增加内置函数的开销,这使得一些答案比其他答案更糟糕。跨度>
  • 您使用profile 的目的是确定代码的哪一部分使其变慢,而不是是否变慢。
  • 另外,为什么你在编辑中提到了 Python 3.x,而不是 PyPy 或 IronPython?如果 8% 的改进值得切换语言版本,那么 700000000% 的改进肯定值得在同一语言版本中切换实现,对吧? :)

标签: python performance list dictionary


【解决方案1】:

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

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

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

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

timeit 结果:

>>> 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

已添加@abarnert 的建议:

def way7(theList, theDict):
    return list(theDict.viewkeys() & theList)

然后重新运行我现在得到的时间:

>>> 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换了地方,我又跑了一遍:

>>> timeit.timeit('way1(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000)
13.648176054011941
>>> timeit.timeit('way6(listOfRandomInts,dictionaryOfRandomInts)', setup=setup, number=1000)
13.847062579316628

所以看起来 set 方法比列表慢,但列表和列表理解之间的区别(令人惊讶的是,至少对我来说)有点可变。我会说随便挑一个,除非它以后成为真正的瓶颈,否则不要担心。

【讨论】:

  • 如果这是 2.7,请尝试 list(theDict.viewkeys() & theList)。在我的机器上,它比你的way5 快大约 10%。 (对于 3.x,您可以使用 keys() 做同样的事情。是的,与 set 不同,& 运算符只接受集合,但对于任何可迭代的、类似集合的视图都有一个 intersection 方法& 运算符采用任何可迭代但没有 intersection 方法...)
  • 您的way6 返回匹配的键;他的way1 返回与匹配键对应的值。但是他的way3也是如此,所以……我不确定他到底想要什么。
  • @abarnert 我没有发现......一旦你有了钥匙,我想,很容易获得相关的价值!我已根据您的建议更新了我的答案。
  • 在 3.4 和 keys 上测量“快 10%”。在 2.7 和 viewkeys 上重复它,无论哪种方式,它都在 1% 以内,就像你发现的那样。看起来它在3.2中也没有任何收获。我不确定最近的 dict 更改中的哪些会影响这一点(拆分哈希表、哈希随机化……似乎都不相关……)。
【解决方案2】:

首先,我认为您使用的是 2.7,所以我将使用 2.7 完成大部分工作。但值得注意的是,如果您真的对优化代码感兴趣,3.x 分支会继续获得性能改进,而 2.x 分支永远不会。为什么你使用 CPython 而不是 PyPy?


无论如何,一些进一步的微优化可以尝试(除了jonrsharpe's answer 中的那些:


在局部变量中缓存属性和/或全局查找(它被称为LOAD_FAST 是有原因的)。例如:

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__,可能不值得这样做。当然,除非您尝试,否则您不会知道:

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 中,我也不希望有任何好处,原因与显式循环中的相同。但让我们试着确定一下:

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

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 方法,但它的& 运算符接受迭代,不像set,它有一个接受迭代的intersection 方法,但它的& 只接受集合. 这很烦人,但我们只关心这里的性能,对吧?)

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

最后一个没有帮助(尽管使用 Python 3.4 而不是 2.7,它快了 10%……),但第一个肯定有。

在现实生活中,您可能还想比较两个集合的大小来决定设置哪个集合,但这里的信息是静态的,所以没有必要编写代码来测试它。


无论如何,我最快的结果是 2.7 上的 map(filter(…)),以相当大的优势。在 3.4(我没有在这里展示)上,Jon 的 listcomp 最快(甚至固定返回值而不是键),并且比任何 2.7 方法都快。此外,3.4 中最快的集合操作(​​使用键视图作为集合,列表作为可迭代对象)比 2.7 更接近迭代方法。

【讨论】:

  • 列表推导在其自己的范围内运行,这仅适用于 Python 3.x,在 Python 2.7 中仍然不是这种情况。
  • 请注意,对于dict.viewkeys(),我们没有显式转换任何内容,但在内部它等效于res = set(dict); intersection_update(res, iterable),这里intersection_update 现在在resiterable 上调用set_intersection。因此与way3a 的性能几乎相同。
  • @AshwiniChaudhary:你确定viewkeys 或其__and__ 方法的作用与res = set(dict) 相同吗? 3.4 的 keys 对象并非如此,因此这可以解释为什么 3.4 获得了显着的加速,而 2.7 却没有……
  • @AshwiniChaudhary:没关系,I found it,你是对的。很好的收获。
  • @AshwiniChaudhary:另外,2.7 listcomp 的东西也不错。我没有写下我想说的话,所以这是完全错误的……已修复。
【解决方案3】:
$ 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%。)这并不是因为 Apple 的 2.7 不好或其他什么,只是 3.x 在过去 5 年多的时间里不断得到优化,而 2.7 没有(以后也不会)。

同时:

$ 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 个额外字符,速度就会提高 7000000 倍。 :)

我确定这是作弊。 JIT 隐含地记住了结果,或者它注意到我什至没有查看结果并将其推到链上并意识到它不需要执行任何步骤或其他什么。但这实际上有时会发生在现实生活中;我有一大堆代码,花了 3 天时间调试并尝试优化,然后才意识到它所做的一切都是不必要的……

无论如何,10 倍数量级的加速比 PyPy 非常典型,即使它不能作弊。而且这比调整属性查找或将谁变成一个集合的顺序颠倒 5% 容易得多。

Jython 更难以预测——有时几乎与 PyPy 一样快,有时比 CPython 慢得多。不幸的是,timeit 在 Jython 2.5.3 中被破坏了,我刚刚通过从 rc2 升级到 rc3 完全破坏了我的 Jython 2.7,所以……今天没有测试。类似地,IronPython 基本上是在不同的 VM 上重做 Jython;它通常更快,但又不可预测。但是我当前版本的 Mono 和我当前版本的 IronPython 并不能很好地配合使用,所以也没有测试。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-12
    • 2014-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多