Подтвердить что ты не робот

Каков самый быстрый способ получить произвольный элемент из словаря Python?

У меня есть dict с приблизительно 17 000 ключами. Я хотел бы выбрать один ключ за раз - неважно, какой из них, и мне не нужно, чтобы это происходило в каком-то конкретном порядке (случайный - это хорошо). Однако после того, как я выберу ключ, я изменю словарь, возможно, добавив или удалив ключ, прежде чем выбрать другой. Поэтому у меня нет набора ключей, через которые я могу выполнить итерацию.

Так как мне не нужно обращаться к ним в каком-либо конкретном порядке, я мог бы каждый раз преобразовывать ключи dict в список, а затем добавлять первый элемент. Тем не менее, поскольку имеется 17 000 ключей, составление списка занимает примерно 0,0005-7 секунд для каждой итерации, что потребует слишком много времени для того, что мне нужно. Есть ли ярлык, который я мог бы сделать так, чтобы мне не приходилось составлять огромный список из ключей dict каждый раз, когда я хочу выбрать один ключ?

4b9b3361

Ответ 1

Существует несколько способов, но вам нужно сделать некоторые компромиссы. Один из способов - освободить словарь, используя popitem; он является атомарным и будет использовать произвольный порядок. Но он сам модифицирует словарь; какой бы элемент ни был выбран, в нем больше нет. Следующий метод, который приходит на ум, повторяется, как обычно, даже при изменении словаря; порядок элементов может измениться, поэтому вы можете получать предметы сколько угодно раз. Чтобы отслеживать это, вы могли бы создать второй set видимых клавиш. Достаточно дешево добавить ключи к набору, дешево проверить, есть ли в нем каждый элемент, и когда вы прошли весь словарь, вы можете проверить, соответствует ли набор клавишам словаря, чтобы определить, есть ли у вас пропущенные (или удалены). В итоге вы создаете набор ключей, но только один элемент на итерацию; в пессиментальном случае мы модифицируем словарь таким образом, что перед поиском нового элемента мы просматриваем весь набор посещенных элементов.

Есть ли причина, по которой эти данные должны храниться только в словаре? Например, если мы рассмотрим систему, в которой мы перетасовываем песни, мы, возможно, не захотим посетить всю библиотеку, а ограничимся только тем, как недавно была воспроизведена песня. Это можно было бы более эффективно обрабатывать, используя список песен, в которых мы можем прочитать случайный индекс, набор недавно воспроизведенных песен, чтобы избежать дубликатов, и очередь (возможно, в списке или дека) песен, что позволяет нам обновлять набор в порядке (удаление последней записи на каждой итерации). Имейте в виду, что ссылки достаточно дешевы.

Переосмыслив еще один шаг, нам не нужны ключи для проверки дубликатов, если они просто не в наших кандидатах; просто заменяя самую старую пьесу со случайно выбранной следующей песней, как список воспроизведения, так и список кандидатов остаются постоянными, и поиск не требуется, так как песни находятся только в одном из списков.

Другая идея - использовать 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 не может скрыть наследуемые сопоставления; вам нужно будет удалить их из поддерживающих словарей.

Ответ 2

Как упоминается в комментариях SRC, идеальным решением является индексированный словарь, который доступен через randomdict:

Создание 17 000 k, v dict и время выполнения:

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

[2.3373830318450928, 2.284735918045044, 2.2462329864501953]

который дает около 2.2μs/choice.

Другие предлагаемые ответы либо не так быстро, не случайны, либо оба.

Ответ 3

Спасибо, vaultah! Вы предложили:

next(iter(dict)))

Это занимает приблизительно 0,00003 секунды, сокращая время на бит более чем в 10 раз, и поэтому работает так же быстро, как мне нужно.

n1c9, вы также сделали интересное предложение:

dict.popitem()

Это функция, о которой я раньше не знал, но, к сожалению, занимает 0.0002 секунды, а не улучшение по сравнению с моим начальным временем.

Ответ 4

Поскольку 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 loop:

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.