Поиск ключей словаря python

Я хочу знать, как я могу выполнить какой-то индекс для ключей из словаря Python. Словарь содержит ок. 400 000 элементов, поэтому я стараюсь избегать линейного поиска.

По сути, я пытаюсь выяснить, находится ли userinput внутри какого-либо из ключей dict.

for keys in dict:
    if userinput in keys:
        DoSomething()
        break

Это был бы пример того, что я пытаюсь сделать. Есть ли способ поиска более прямым способом, без цикла? или что было бы более эффективным способом.

Пояснение: userinput — это не совсем то, чем будет ключ, например, userinput может быть log, а ключ — logfile.

Редактировать: любое создание списка/кэша, предварительная обработка или организация, которые могут быть выполнены до поиска, допустимы. Единственное, что нужно сделать быстро, это поиск ключа.


person Trent    schedule 02.03.2011    source источник
comment
Каков тип вашего ключа? Я не думаю, что вам нужны две петли.   -  person Mikel    schedule 03.03.2011
comment
Когда все доступные (и реализуемые) алгоритмы имеют неприемлемую сложность, пора переосмыслить проблему или используемую структуру данных. Существуют (относительно экзотические) структуры данных для нечеткого сопоставления строк, хотя я не знаю, существуют ли они для произвольных подстрок.   -  person    schedule 03.03.2011
comment
Если пользовательский ввод всегда будет начинаться с начала ключа, вы можете создать keycache вложенных диктов {'a':{'b':['abacus', 'absinthe'...] ...} ...}   -  person senderle    schedule 03.03.2011
comment
Ключ — это просто строка, например, «logfile».   -  person Trent    schedule 03.03.2011
comment
@Trent: Тогда ваша строка if userinput in keys неверна. Можете ли вы попробовать удалить цикл for, заставить его работать для одного элемента, а затем обновить свой вопрос кодом, который вы имеете в виду?   -  person Mikel    schedule 03.03.2011
comment
Но всегда ли пользовательский ввод находится в начале ключа?   -  person kojiro    schedule 03.03.2011
comment
@Mikel, как это неправильно? Использование множественного числа «ключей» сбивает с толку, но 'foo' in 'foobar' is True. Мне кажется правильным.   -  person kojiro    schedule 03.03.2011
comment
@Trent, @kojiro: Извините, вы правы, я должен был сначала протестировать. Yay для двойственности строки/списка в Python.   -  person Mikel    schedule 03.03.2011
comment
@Trent, мне все еще интересно, всегда ли userinput является префиксом, как в вашем примере «log»/«logfile», или же userinput является произвольной подстрокой, как в «file»/«longfilename» '. Это имеет довольно большое значение.   -  person senderle    schedule 03.03.2011
comment
подстрока :) нужна гибкость   -  person Trent    schedule 03.03.2011


Ответы (6)


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

person Mark Byers    schedule 02.03.2011
comment
Ах, попробуй - думаю, это слово, которое я хотел для своего комментария выше. +1 - person senderle; 03.03.2011
comment
Не будет ли построение самого триа довольно затратным? Вам все равно придется перебирать весь словарь, и в этот момент это не будет существенно лучше, чем наивное решение, и может быть хуже для среднего случая. - person Chinmay Kanchi; 03.03.2011
comment
@Chinmay Kanchi, поначалу это будет дорого, но вам нужно будет сделать это только один раз, верно? Все будущие поиски префиксов будут быстрыми, потому что вы будете искать совпадение или совпадения в дереве. (Предположительно, вы бы представили список потенциальных совпадений, если в дереве их больше одного, или у вас есть какой-то другой алгоритм для выбора одного.) Кроме того, дерево может быть построено заранее и обработано; и если dict изменится, его можно будет довольно легко обновить и снова замариновать при выходе из программы. - person senderle; 03.03.2011
comment
@senderle: Неважно, я опубликовал это до того, как увидел обновленный пост ОП. - person Chinmay Kanchi; 03.03.2011

Если вам нужно найти только ключи, начинающиеся с префикса, вы можете использовать бинарный поиск. Что-то вроде этого выполнит эту работу:

import bisect
words = sorted("""
a b c stack stacey stackoverflow stacked star stare x y z
""".split())
n = len(words)
print n, "words"
print words
print
tests = sorted("""
r s ss st sta stack star stare stop su t
""".split())
for test in tests:
    i = bisect.bisect_left(words, test)
    if words[i] < test: i += 1
    print test, i
    while i < n and words[i].startswith(test):
        print i, words[i]
        i += 1

Вывод:

12 words
['a', 'b', 'c', 'stacey', 'stack', 'stacked', 'stackoverflow', 'star', 'stare',
'x', 'y', 'z']

r 3
s 3
3 stacey
4 stack
5 stacked
6 stackoverflow
7 star
8 stare
ss 3
st 3
3 stacey
4 stack
5 stacked
6 stackoverflow
7 star
8 stare
sta 3
3 stacey
4 stack
5 stacked
6 stackoverflow
7 star
8 stare
stack 4
4 stack
5 stacked
6 stackoverflow
star 7
7 star
8 stare
stare 8
8 stare
stop 9
su 9
t 9
person John Machin    schedule 03.03.2011

Нет. Единственный способ найти строку в словарных ключах — просмотреть каждый ключ. Что-то вроде того, что вы предложили, - единственный способ сделать это со словарем.

Однако, если у вас есть 400 000 записей и вы хотите ускорить поиск, я бы предложил использовать базу данных SQLite. Тогда вы можете просто сказать SELECT * FROM TABLE_NAME WHERE COLUMN_NAME LIKE '%userinput%';. Посмотрите документацию для модуля Python sqlite3 здесь.

Другой вариант — использовать генераторное выражение, так как оно почти всегда быстрее, чем эквивалентные циклы for.

filteredKeys = (key for key in myDict.keys() if userInput in key)
for key in filteredKeys:
    doSomething()

EDIT: Если, как вы говорите, вас не волнуют разовые затраты, используйте базу данных. SQLite должен делать то, что вы хотите, почти идеально.

Я провел несколько тестов, и, к моему удивлению, наивный алгоритм на самом деле в два раза быстрее, чем версия, использующая понимание списков, и в шесть раз быстрее, чем версия, управляемая SQLite. В свете этих результатов мне пришлось бы пойти с @Mark Byers и порекомендовать Trie. Я разместил тест ниже, на случай, если кто-то захочет попробовать.

import random, string, os
import time
import sqlite3

def buildDict(numElements):
    aDict = {}
    for i in xrange(numElements-10):
        aDict[''.join(random.sample(string.letters, 6))] = 0

    for i in xrange(10):
        aDict['log'+''.join(random.sample(string.letters, 3))] = 0

    return aDict

def naiveLCSearch(aDict, searchString):
    filteredKeys = [key for key in aDict.keys() if searchString in key]
    return filteredKeys

def naiveSearch(aDict, searchString):
    filteredKeys = []
    for key in aDict:
        if searchString in key: 
            filteredKeys.append(key)
    return filteredKeys

def insertIntoDB(aDict):
    conn = sqlite3.connect('/tmp/dictdb')
    c = conn.cursor()
    c.execute('DROP TABLE IF EXISTS BLAH')
    c.execute('CREATE TABLE BLAH (KEY TEXT PRIMARY KEY, VALUE TEXT)')
    for key in aDict:
        c.execute('INSERT INTO BLAH VALUES(?,?)',(key, aDict[key]))
    return conn

def dbSearch(conn):
    cursor = conn.cursor()
    cursor.execute("SELECT KEY FROM BLAH WHERE KEY GLOB '*log*'")
    return [record[0] for record in cursor]

if __name__ == '__main__':
    aDict = buildDict(400000)
    conn = insertIntoDB(aDict)
    startTimeNaive = time.time()
    for i in xrange(3):
        naiveResults = naiveSearch(aDict, 'log')
    endTimeNaive = time.time()
    print 'Time taken for 3 iterations of naive search was', (endTimeNaive-startTimeNaive), 'and the average time per run was', (endTimeNaive-startTimeNaive)/3.0

    startTimeNaiveLC = time.time()
    for i in xrange(3):
        naiveLCResults = naiveLCSearch(aDict, 'log')
    endTimeNaiveLC = time.time()
    print 'Time taken for 3 iterations of naive search with list comprehensions was', (endTimeNaiveLC-startTimeNaiveLC), 'and the average time per run was', (endTimeNaiveLC-startTimeNaiveLC)/3.0

    startTimeDB = time.time()
    for i in xrange(3):
        dbResults = dbSearch(conn)
    endTimeDB = time.time()
    print 'Time taken for 3 iterations of DB search was', (endTimeDB-startTimeDB), 'and the average time per run was', (endTimeDB-startTimeDB)/3.0


    os.remove('/tmp/dictdb')

Для справки, мои результаты были такими:

Time taken for 3 iterations of naive search was 0.264658927917 and the average time per run was 0.0882196426392
Time taken for 3 iterations of naive search with list comprehensions was 0.403481960297 and the average time per run was 0.134493986766
Time taken for 3 iterations of DB search was 1.19464492798 and the average time per run was 0.398214975993

Все время указано в секундах.

person Chinmay Kanchi    schedule 02.03.2011
comment
Я бы сократил приведенное выше до «Однако, если у вас есть 400 000 записей, используйте базу данных. - person senderle; 03.03.2011
comment
Другой вариант — использовать списки, так как они почти всегда быстрее, чем эквивалентные циклы. Источник? - person Mikel; 03.03.2011
comment
@Mikel, я не знаю источника, но я считаю, что Чинмей прав, просто исходя из опыта. - person senderle; 03.03.2011
comment
@Mikel: wiki.python.org/moin/PythonSpeed/PerformanceTips . Посмотрите в разделе Циклы. - person Chinmay Kanchi; 03.03.2011
comment
@Chinmay Но мы не знаем, нужно ли ему обрабатывать весь набор ключей. Выполнение этого понимания списка, вероятно, будет дорогостоящим, если userInput вообще не находится в ключах. Я предлагаю использовать генератор вместо понимания списка. Затем вы можете вырваться после того, как будет найдено первое совпадение. - person kojiro; 03.03.2011
comment
@kojiro: Хороший вопрос, отредактировал мой ответ. В худшем случае все равно будет то же самое, поскольку порядок ключей не гарантируется, но в среднем и лучшем случаях он должен быть намного лучше. - person Chinmay Kanchi; 03.03.2011
comment
@Chinmay: убери .keys() - person John Machin; 03.03.2011
comment
@John: я предпочитаю оставить его там, так как считаю, что это делает код более очевидным в отношении его намерений. Хотя я понимаю, что это не обязательно. - person Chinmay Kanchi; 03.03.2011
comment
@Chinmay, к сожалению (с точки зрения удобочитаемости), я думаю, что Джон прав; in mydict.keys() ищет в списке, а не в словаре, и работает ужасно медленно. (Не то, чтобы это сейчас имело значение!) - person senderle; 03.03.2011
comment
Смущает, что понимание списка медленнее ... вы пробовали это с генератором? - person senderle; 03.03.2011
comment
Ой! Вы также создаете список .keys(). Я проверил это с filteredKeys = [key for key in aDict if searchString in key], и это была почти такая же скорость, как и в простом цикле. Затем я протестировал его с помощью filteredKeys = (key for key in aDict.keys() if searchString in key), и он оказался на четыре порядка быстрее. Потом я понял, что это потому, что генератор на самом деле не работает :). Ну что ж. - person senderle; 03.03.2011

Вы можете объединить все ключи в одну длинную строку с подходящим символом-разделителем и использовать метод find строки. Это довольно быстро.

Возможно, этот код будет полезен для вас. Метод search возвращает список значений словаря, ключи которых содержат подстроку key.

class DictLookupBySubstr(object):
    def __init__(self, dictionary, separator='\n'):
        self.dic = dictionary
        self.sep = separator
        self.txt = separator.join(dictionary.keys())+separator

    def search(self, key):
        res = []
        i = self.txt.find(key)
        while i >= 0:
            left = self.txt.rfind(self.sep, 0, i) + 1
            right = self.txt.find(self.sep, i)
            dic_key = self.txt[left:right]
            res.append(self.dic[dic_key])
            i = self.txt.find(key, right+1)
        return res
person Janne Karila    schedule 03.03.2011

dpath может легко решить эту проблему.

http://github.com/akesterson/dpath-python

$ easy_install dpath
>>> for (path, value) in dpath.util.search(MY_DICT, "glob/to/start/{}".format(userinput), yielded=True):
>>> ...    # (do something with the path and value)

Вы можете передать eglob ('path//to//something/[0-9a-z]') для расширенного поиска.

person Andrew Kesterson    schedule 12.05.2013

Возможно, использование has_key также решит эту проблему.

http://docs.python.org/release/2.5.2/lib/typesmapping.html

person neosergio    schedule 28.02.2012