2016-11-29 2 views
13

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

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

+5

https://docs.python.org/3/library/stdtypes.html#dict.popitem – n1c9

+7

Рассматривали ли вы 'next (iter (dct))'? – vaultah

+2

Вот хороший фрагмент кода, который делает именно это, но с временной сложностью O (1). Поскольку вы беспокоитесь о времени, вам может быть лучше использовать это - https://github.com/robtandy/randomdict – SRC

ответ

6

Существует несколько способов, но вам нужно будет сделать некоторые компромиссы. Один из способов - освободить словарь, используя 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 

Как это означает, что оба словаря постоянно меняются, это, вероятно, не самый быстрый в целом, но он поддерживает как коллекцию dictionarylike и возможность обрабатывать все элементы. Он утрачивает возможность прямого удаления элементов, поскольку ChainMap не может скрыть наследуемые сопоставления; вам нужно будет удалить их из поддерживающих словарей.

+0

, если элемент должен оставаться в dict, вы можете просто повторно добавить его непосредственно после '' popitem() '' –

+1

"Следующий метод, который приходит на ум, - это итерация, как обычно, даже при изменении словаря "- не работает. Даже если словарь не будет восстановлен частично, вы, вероятно, инициируете проверку безопасности «измененный размер словаря во время итерации». – user2357112

+0

Вы могли бы, но вопрос в том, насколько строгим является «выбор другого * одного». Вызов popitem после того, как вы сделали или не сделали другие изменения, могут повторять один и тот же элемент. Если бы мы переходили между двумя коллекциями, это могло бы работать достаточно хорошо. –

1

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

next(iter(dict))) 

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

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

dict.popitem() 

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

+1

Стоит отметить, что нет гарантии случайности, используя любой из этих методов. – TemporalWolf

+4

Ваши тайминги не справедливы для 'popitem' ... имейте в виду, что он также * удаляет элемент *, что означает, что' popitem' выполняет поиск * и * обновление, а 'next (iter (dct))' only выполняет поиск, но если вам нужно удалить этот элемент, вы в конечном итоге оплатите операцию удаления! Если * most * из элементов в конечном итоге удаляется, то 'popitem' должен быть более быстрым в целом, только если несколько« popitem »сделают слишком много изменений. Дело в том, что не просто профилируйте одну операцию, проведите весь цикл! – Bakuriu

+0

Также стоит упомянуть полное решение (подклассификация dict для добавления неупорядоченного индекса ключей, который может быть «random.choice''d в« O (1) », для этого требуется' printtimeit.timeit («lambda: random.choice (dog_dict) ", number = 1) >>> 0.0000031s', (где dog_dict содержит 17000 k, v пар), что на порядок выше, хотя вставки и удаления становятся немного более дорогостоящими, так как вы должны поддерживать index – TemporalWolf

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() и в то время как цикл:

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. 

+0

Обратите внимание, что '.pop (0)' работает только в python2 ... в python3 'keys' возвращает объект' set', который не упорядочен. – Bakuriu

+0

Ну тогда просто используйте item = keys [0]; del keys [0]. Но в этом случае и в случае с pop() тоже лучше использовать последний элемент: keys [-1], он будет быстрее. Списки не предназначены для быстрого редактирования, а быстрый доступ к произвольной памяти. – Dalen

+0

Метод keys() строит большой список OP, который жаловался (или список (d.keys()) в Python 3), а поверх этого list.pop (0) - O (n). list.pop() займет последний элемент, который быстрее, чем другие не нужно перемещать. По мере роста используемого набора повторные случайные варианты поиска последних предметов, не входящих в него, дорожают. –

3

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

Построение 17000 к, v Dict и работает timeit:

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

[2.3373830318450928, 2.284735918045044, 2.2462329864501953]

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

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

Смежные вопросы