从Python字典中获取任意元素的最快方法是什么?

时间:2016-11-29 17:45:20

标签: python dictionary

我有大约17,000个键的字典。我想一次选择一个密钥 - 哪一个并不重要,我不需要以任何特定的顺序发生(随机就好了)。但是,在我选择一个键之后,我会在选择另一个键之前更改字典,可能是通过添加或删除键。因此,我没有可以迭代的一组键列表。

由于我不需要以任何特定的顺序访问它们,我每次都可以将dict键转换为列表,然后弹出第一个元素。但是,由于有17,000个键,因此在每次迭代时制作一个列表大约需要0.0005-7秒,这将花费我太多的时间来满足我的需要。有没有我可以采用的快捷方式,以便每次我想选择一个密钥时都不需要用dict键编译一个庞大的列表?

4 个答案:

答案 0 :(得分:6)

有多种方法,但您需要做出一些权衡。一种方法是使用popitem清空字典;它是原子的,并将使用任意顺序。但它修改了字典本身;选择的任何项目都不在其中。想到的下一个方法是像往常一样迭代,即使在修改字典时也是如此;项目的顺序可能会发生变化,因此您可以多次获取项目。要跟踪它,您可以构建第二个set可见键。将密钥添加到集合中是相当便宜的,检查每个项目是否在其中是否便宜,当您浏览整个字典时,您可以检查该集合是否与字典的密钥匹配,以确定是否存在您错过的密钥(或去除)。你最终建立一个密钥集,但每次迭代只有一个项目;在pessimal情况下,我们以这样的方式修改字典,我们在找到新项目之前扫描整个被访问项目集。

这个数据是否只需要保存在字典中?例如,如果我们考虑一个我们正在洗牌的系统,我们可能不想访问整个图书馆,但只限制一首歌的播放时间。使用歌曲列表可以更有效地处理,其中我们可以读取随机索引,一组最近播放的歌曲以避免重复,以及歌曲的队列(可能在列表或双端队列中)允许我们按顺序更新集合(每次迭代删除最后一个条目)。请记住,引用相当便宜。

如果他们根本不在我们的候选人中,我们不再需要钥匙来检查重复项;通过用随机选择的下一首歌曲交换最老的播放歌曲,播放和候选列表都保持恒定大小并且不需要查找,因为歌曲仅在一个列表中。

另一个想法是使用collections.ChainMap来保持对两个词典的一致视图;已访问过的和未访问过的。然后,您可以通过popitem将项目从后者迁移到前者,确保处理集合中所有内容的可读方法,同时保持字典类似。

def getnewitem(chainmap):
    # Raises KeyError when finished
    key,value=chainmap.maps[0].popitem()
    chainmap.maps[1][key]=value
    return key,value

因为这意味着两个字典都在不断变化,它可能不是最快的整体,但它既保持字典集合,又保持处理所有项目的能力。它确实失去了直接删除项目的能力,因为ChainMap无法隐藏继承的映射;你需要从支持词典中删除它们。

答案 1 :(得分:3)

正如评论中提到的SRC,理想的解决方案是索引字典,可通过randomdict获得:

建立一个17,000 k,v dict和运行timeit:

t = timeit.Timer(my_dict.random_item)
print t.repeat()
  

[2.3373830318450928,2.284735918045044,2.2462329864501953]

给出约2.2μs/选择。

其他建议的答案要么不是快,要么不是随机,或两者兼而有之。

答案 2 :(得分:1)

谢谢你,vaultah!你提议:

next(iter(dict)))

大约需要0.00003秒,时间缩短10倍以上,因此工作速度可以达到我需要的速度。

n1c9,你也提出了一个有趣的建议:

dict.popitem()

这是我之前不知道的功能,但遗憾的是需要0.0002秒,与我的初始时间相比并没有太大的改进。

答案 3 :(得分:0)

由于dict()是根据用于快速访问的内部哈希值而不是按向其添加元素的顺序排序的,因此您可以将其视为足够随机并使用dict.popitem()。

但是popitem()也会从字典中删除这个元素。所以你可能希望使用:

d = {...}
keys = d.keys()
item = keys.pop(0)
value = d[item]

代替。但请注意,具有相同/相似键的任何dict可能具有相同的键顺序。

如果你想获得适当的随机性,那就去做:

import random
d = {"red": "#ff0000", "green": "#00ff00", "blue": "#0000ff", "black": "#000000", "white": "#ffffff"}
keys = d.keys()
item = random.choice(keys)
value = d[item]

当然,如果你想防止重复,你将不得不使用额外的dict()和while循环:

import random
d = {"red": "#ff0000", "green": "#00ff00", "blue": "#0000ff", "black": "#000000", "white": "#ffffff"}
keys = d.keys()
used = {}
def get_rand_item (d):
    item = random.choice(keys)
    while item in used:
        item = random.choice(keys)
    value = d[item]
    used[item] = None
    return item, value
get_rand_item(d)

这里我使用dict作为存储,因为它的搜索速度比列表快。

你问了最快的方法。 :d

当我在它的时候,让我看看我是否可以更快地获得随机项目而不重复:



from random import choice

class RandomGetter:
    def __init__ (self, d, adapt=1):
        self.keys = keys = d.keys()
        if adapt:
            # Could be done in place too
            dct = {}
            for k in keys:
                dct[k] = (d[k], 0)
            self.dct = dct
            self.count = 1
        else:
            self.dct = d
            # Assume all items have been visited
            self.count = d[keys[0]][1]+1
        self.visited = 0
        self.length = len(self.dct)

    def __len__ (self):
        return self.length

    def randitem (self, default=None):
        if self.visited==self.length:
            # After 'default' is returned (all items gotten),
            # same RandomGetter() can be used again:
            self.count += 1
            self.visited = 0
            return default
        d  = self.dct
        kz = self.keys
        c  = self.count
        key = choice(kz)
        value, flag = d[key]
        while flag>=c:
            key = choice(kz)
            value, flag = d[key]
        d[key] = (value, flag+1)
        self.visited += 1
        return key, value

    def next (self):
        i = self.randitem()
        if i==None: raise StopIteration
        return i

    def __iter__ (self):
        while 1: yield self.next()

# Now testing:
# Lets create a dictionary of one million items:
d = dict.fromkeys(xrange(1000000))
# This takes about 0.128 seconds
# Now, lets initialize Rg
r = RandomGetter(d)
# If dict is not prepared in advance, as this one isn't we use adapt=1 and it takes
# about 8.92 seconds. Yack!
# Now measure time for each random getting:
from time import time
def check ():
    randitem = r.randitem # Faster access to the method
    e = []
    for _ in xrange(len(r)):
        t = time()
        randitem()
        e.append(time()-t)
    print "Total/min/max/med/avg/(0 time count)"
    e.sort()
    s = sum(e)
    if len(r)%2==0: m = (e[len(r)/2]+e[len(r)/2+1])/2
    else: m = e[len(r)/2+1]
    print s, e[0], e[-1], m, ("%.15f" % (s/1000000)), e.count(0.0)
check()
# It yields following results on my machine:
# About 25.224 seconds to randomly get all 1000000 items
# Minimal time needed is not measurable using this technique so it is 0.0
# Maximal time needed to get an item is about 1.678 seconds
# Median results with 0.0, thus we know that more than half randomly gotten items took practically no time
# In fact, there are about 998808 items with getting time of 0.0 seconds
# Average getting time is about 0.000025224 seconds
# By examining results closely I deduced that only last few items took a long time to get them.
# All in all, not bad for one million items, in my opinion anyway.
# For dict of 2000 items, total time was 0.016 and that was also the maximal value and it was for the last gotten item
# Time didn't cross one second until length of a given dictionary wasn't bigger than 100000
# If you want, you can run my code through timeit to recheck, but it seems that it is faster
# than suggested random dictionary.