Зворотний пошук словника в Python


102

Чи існує прямий спосіб пошуку ключа, знаючи значення в словнику?

Все, що я можу придумати, це:

key = [key for key, value in dict_obj.items() if value == 'value'][0]

можливо дублікат: stackoverflow.com/questions/483666 / ...
Tobias Kienzler


Google мене керував тут ... І я повинен сказати .. чому ніхто не використовує, iteritemsяк для мене, це робить різницю на 40 разів швидше ... використовуючи метод () .dext
Злий 84,

4
Якщо у вас є багато зворотних пошуків:reverse_dictionary = {v:k for k,v in dictionary.items()}
Остін,

Відповіді:


5

Немає жодної. Не забувайте, що значення може бути знайдено в будь-якій кількості клавіш, включаючи 0 або більше 1.


2
python має метод .index у списках, що повертає перший знайдений індекс із вказаним значенням або виняток, якщо його не знайдено ... будь-яка причина, чому така семантика не може бути застосована до словників?
Брайан Джек

@BrianJack: Словники не впорядковані, як набори. Подивіться на collections.OrderedDict для реалізації , що є впорядкованою.
Martijn Pieters

3
.index повинен лише гарантувати, що він повертає єдине значення, і його не потрібно бути лексично спочатку лише тим, що це перша відповідність, і що його поведінка є стабільною (багаторазові дзвінки в одному і тому ж диктаті з часом повинні давати однаковий елемент). Якщо словники з часом не переставляють свої немодифіковані хеші, коли інші елементи додаються, видаляються чи змінюються, вони все одно працюватимуть належним чином. Наївна реалізація: dictObject.items (). Index (key)
Брайан Джек

Справа в основному .index () полягає в тому, що за визначенням нас не хвилюють дублікати, лише щоб ми могли шукати один елемент послідовно
Брайан Джек,

130
Я ненавиджу такі відповіді, як це. "Перестаньте намагатися робити те, що виправдано хочете зробити!" це НЕ є прийнятним відповіддю. Чому це було прийнято? Як свідчать відповіді вище на це запитання, пошук зворотного словника тривіально реалізований менш ніж у 80 символів чистого Python. Це не стає більше "прямо", ніж це. Рішення Пола Макгуайра , мабуть, найефективніше, але всі вони працюють. </sigh>
Сесіль Карі

95

Розуміння вашого списку проходить через усі елементи диктанту, знаходячи всі збіги, а потім просто повертає перший ключ. Цей вираз генератора лише повторить, наскільки це необхідно для повернення першого значення:

key = next(key for key, value in dd.items() if value == 'value')

де ddдикт. Піднімемо , StopIterationякщо збіг не знайдено, так що ви можете зловити , що і повертати більш відповідне виключення , як ValueErrorабо KeyError.


1
Так, ймовірно, це повинно створювати той самий виняток, що і listObject.index (ключ), коли ключа немає у списку.
Брайан Джек

7
також keys = { key for key,value in dd.items() if value=='value' }отримати набір усіх клавіш, якщо кілька збігів.
askewchan

6
@askewchan - немає реальної необхідності повертати це як набір, клавіші dict вже повинні бути унікальними, просто поверніть список - а ще краще - поверніть вираз генератора, і хай абонент покладе його в будь-який контейнер, який вони хочуть.
PaulMcG

55

Бувають випадки, коли словник один: одне зіставлення

Наприклад,

d = {1: "one", 2: "two" ...}

Ваш підхід нормальний, якщо ви робите лише один пошук. Однак якщо вам потрібно зробити кілька пошукових запитів, то буде більш ефективно створити зворотний словник

ivd = {v: k for k, v in d.items()}

Якщо є можливість декількох клавіш з однаковим значенням, вам потрібно буде вказати бажану поведінку в цьому випадку.

Якщо ваш Python 2,6 або старший, ви можете використовувати його

ivd = dict((v, k) for k, v in d.items())

6
Приємна оптимізація. Але, я думаю, ви мали намір перетворити свій список 2-кортезів у словник за допомогою dict ():ivd=dict([(v,k) for (k,v) in d.items()])
варильні панелі

4
@hobs просто використовує розуміння диктату замість розуміння списку:invd = { v:k for k,v in d.items() }
askewchan

Поняття @gnibbler dict не були перенесені назад на Python 2.6, тому, якщо ви хочете залишатися портативними, вам потрібно буде помиритися з 6 додатковими символами для dict () навколо генератора 2-кортежів або розуміння списку 2 -tuples
варильні панелі

@hobs, я додав це до своєї відповіді.
Джон Ла Рой

32

Ця версія на 26% коротша від вашої, але функціонує однаково, навіть для надмірних / неоднозначних значень (повертає першу відповідність, як і ваша). Однак це, мабуть, удвічі повільніше, ніж у вас, бо він створює список із диктату вдвічі.

key = dict_obj.keys()[dict_obj.values().index(value)]

Або якщо ви віддаєте перевагу стислості над читабельністю, ви можете зберегти ще одного символу

key = list(dict_obj)[dict_obj.values().index(value)]

І якщо ви віддаєте перевагу ефективності, підхід @ PaulMcGuire краще. Якщо є багато ключів, які мають одне і те ж значення, то ефективніше не інстанціювати цей список ключів зі списком розуміння, а замість цього використовувати генератор:

key = (key for key, value in dict_obj.items() if value == 'value').next()

2
Припустимо, що атомна операція гарантує, що ключі та значення будуть у тому ж відповідному порядку?
Noctis Skytower

1
@NoctisSkytower Так, dict.keys()і dict.values()вони гарантовано відповідатимуть, доки dictне буде змінено між дзвінками.
варильні панелі

7

Оскільки це все ще дуже актуально, першим хітом Google, і я просто витрачаю деякий час на з'ясування цього, я опублікую своє (працююче в Python 3) рішення:

testdict = {'one'   : '1',
            'two'   : '2',
            'three' : '3',
            'four'  : '4'
            }

value = '2'

[key for key in testdict.items() if key[1] == value][0][0]

Out[1]: 'two'

Це дасть вам перше значення, яке відповідає.


6

Можливо, клас, схожий на словник, як DoubleDictвнизу, є те, що ви хочете? Ви можете використовувати будь-який із наданих метакласів спільно з DoubleDictабо взагалі уникати використання будь-якого метакласу.

import functools
import threading

################################################################################

class _DDChecker(type):

    def __new__(cls, name, bases, classdict):
        for key, value in classdict.items():
            if key not in {'__new__', '__slots__', '_DoubleDict__dict_view'}:
                classdict[key] = cls._wrap(value)
        return super().__new__(cls, name, bases, classdict)

    @staticmethod
    def _wrap(function):
        @functools.wraps(function)
        def check(self, *args, **kwargs):
            value = function(self, *args, **kwargs)
            if self._DoubleDict__forward != \
               dict(map(reversed, self._DoubleDict__reverse.items())):
                raise RuntimeError('Forward & Reverse are not equivalent!')
            return value
        return check

################################################################################

class _DDAtomic(_DDChecker):

    def __new__(cls, name, bases, classdict):
        if not bases:
            classdict['__slots__'] += ('_DDAtomic__mutex',)
            classdict['__new__'] = cls._atomic_new
        return super().__new__(cls, name, bases, classdict)

    @staticmethod
    def _atomic_new(cls, iterable=(), **pairs):
        instance = object.__new__(cls, iterable, **pairs)
        instance.__mutex = threading.RLock()
        instance.clear()
        return instance

    @staticmethod
    def _wrap(function):
        @functools.wraps(function)
        def atomic(self, *args, **kwargs):
            with self.__mutex:
                return function(self, *args, **kwargs)
        return atomic

################################################################################

class _DDAtomicChecker(_DDAtomic):

    @staticmethod
    def _wrap(function):
        return _DDAtomic._wrap(_DDChecker._wrap(function))

################################################################################

class DoubleDict(metaclass=_DDAtomicChecker):

    __slots__ = '__forward', '__reverse'

    def __new__(cls, iterable=(), **pairs):
        instance = super().__new__(cls, iterable, **pairs)
        instance.clear()
        return instance

    def __init__(self, iterable=(), **pairs):
        self.update(iterable, **pairs)

    ########################################################################

    def __repr__(self):
        return repr(self.__forward)

    def __lt__(self, other):
        return self.__forward < other

    def __le__(self, other):
        return self.__forward <= other

    def __eq__(self, other):
        return self.__forward == other

    def __ne__(self, other):
        return self.__forward != other

    def __gt__(self, other):
        return self.__forward > other

    def __ge__(self, other):
        return self.__forward >= other

    def __len__(self):
        return len(self.__forward)

    def __getitem__(self, key):
        if key in self:
            return self.__forward[key]
        return self.__missing_key(key)

    def __setitem__(self, key, value):
        if self.in_values(value):
            del self[self.get_key(value)]
        self.__set_key_value(key, value)
        return value

    def __delitem__(self, key):
        self.pop(key)

    def __iter__(self):
        return iter(self.__forward)

    def __contains__(self, key):
        return key in self.__forward

    ########################################################################

    def clear(self):
        self.__forward = {}
        self.__reverse = {}

    def copy(self):
        return self.__class__(self.items())

    def del_value(self, value):
        self.pop_key(value)

    def get(self, key, default=None):
        return self[key] if key in self else default

    def get_key(self, value):
        if self.in_values(value):
            return self.__reverse[value]
        return self.__missing_value(value)

    def get_key_default(self, value, default=None):
        return self.get_key(value) if self.in_values(value) else default

    def in_values(self, value):
        return value in self.__reverse

    def items(self):
        return self.__dict_view('items', ((key, self[key]) for key in self))

    def iter_values(self):
        return iter(self.__reverse)

    def keys(self):
        return self.__dict_view('keys', self.__forward)

    def pop(self, key, *default):
        if len(default) > 1:
            raise TypeError('too many arguments')
        if key in self:
            value = self[key]
            self.__del_key_value(key, value)
            return value
        if default:
            return default[0]
        raise KeyError(key)

    def pop_key(self, value, *default):
        if len(default) > 1:
            raise TypeError('too many arguments')
        if self.in_values(value):
            key = self.get_key(value)
            self.__del_key_value(key, value)
            return key
        if default:
            return default[0]
        raise KeyError(value)

    def popitem(self):
        try:
            key = next(iter(self))
        except StopIteration:
            raise KeyError('popitem(): dictionary is empty')
        return key, self.pop(key)

    def set_key(self, value, key):
        if key in self:
            self.del_value(self[key])
        self.__set_key_value(key, value)
        return key

    def setdefault(self, key, default=None):
        if key not in self:
            self[key] = default
        return self[key]

    def setdefault_key(self, value, default=None):
        if not self.in_values(value):
            self.set_key(value, default)
        return self.get_key(value)

    def update(self, iterable=(), **pairs):
        for key, value in (((key, iterable[key]) for key in iterable.keys())
                           if hasattr(iterable, 'keys') else iterable):
            self[key] = value
        for key, value in pairs.items():
            self[key] = value

    def values(self):
        return self.__dict_view('values', self.__reverse)

    ########################################################################

    def __missing_key(self, key):
        if hasattr(self.__class__, '__missing__'):
            return self.__missing__(key)
        if not hasattr(self, 'default_factory') \
           or self.default_factory is None:
            raise KeyError(key)
        return self.__setitem__(key, self.default_factory())

    def __missing_value(self, value):
        if hasattr(self.__class__, '__missing_value__'):
            return self.__missing_value__(value)
        if not hasattr(self, 'default_key_factory') \
           or self.default_key_factory is None:
            raise KeyError(value)
        return self.set_key(value, self.default_key_factory())

    def __set_key_value(self, key, value):
        self.__forward[key] = value
        self.__reverse[value] = key

    def __del_key_value(self, key, value):
        del self.__forward[key]
        del self.__reverse[value]

    ########################################################################

    class __dict_view(frozenset):

        __slots__ = '__name'

        def __new__(cls, name, iterable=()):
            instance = super().__new__(cls, iterable)
            instance.__name = name
            return instance

        def __repr__(self):
            return 'dict_{}({})'.format(self.__name, list(self))

4

Ні, ви не можете зробити це ефективно, не заглянувши в усі ключі та перевіривши всі їх значення. Тож для цього вам знадобиться O(n)час. Якщо вам потрібно зробити багато таких пошукових запитів, вам доведеться це зробити ефективно, побудувавши зворотний словник (це можна зробити і в O(n)), а потім здійснити пошук всередині цього зворотного словника (кожен пошук буде тривати в середньому O(1)).

Ось приклад того, як побудувати зворотний словник (який зможе зробити від одного до багатьох зіставлення) з звичайного словника:

for i in h_normal:
    for j in h_normal[i]:
        if j not in h_reversed:
            h_reversed[j] = set([i])
        else:
            h_reversed[j].add(i)

Наприклад, якщо ваш

h_normal = {
  1: set([3]), 
  2: set([5, 7]), 
  3: set([]), 
  4: set([7]), 
  5: set([1, 4]), 
  6: set([1, 7]), 
  7: set([1]), 
  8: set([2, 5, 6])
}

ваша h_reversedбуде

{
  1: set([5, 6, 7]),
  2: set([8]), 
  3: set([1]), 
  4: set([5]), 
  5: set([8, 2]), 
  6: set([8]), 
  7: set([2, 4, 6])
}

2

Наскільки я знаю, немає жодного способу, однак один спосіб зробити це - створити дикт для звичайного пошуку за ключем, а інший - для зворотного пошуку за значенням.

Тут є приклад такої реалізації:

http://code.activestate.com/recipes/415903-two-dict-classes-which-can-lookup-keys-by-value-an/

Це означає, що пошук ключових значень може призвести до отримання декількох результатів, які можна повернути як простий список.


Зверніть увагу, що існує багато, багато можливих значень, які не є дійсними ключами.
Ігнасіо Васкес-Абрамс

1

Я знаю, що це може вважатися "марнотратним", але в цьому сценарії я часто зберігаю ключ як додатковий стовпець у записі значень:

d = {'key1' : ('key1', val, val...), 'key2' : ('key2', val, val...) }

це компроміс і відчуває себе неправильно, але це просто і працює, і, звичайно, залежить від значень, що є кортежами, а не простими значеннями.


1

Складіть зворотний словник

reverse_dictionary = {v:k for k,v in dictionary.items()} 

Якщо у вас є багато зворотних пошуків


0

Через значення в словнику можуть бути об'єкти будь-якого типу, їх не можна хешировать або індексувати іншим способом. Тому пошук ключа за значенням неприродний для цього типу колекції. Будь-який запит, подібний до цього, може виконуватися лише за O (n) час. Отже, якщо це часте завдання, вам слід поглянути на деяку індексацію ключа, наприклад, Jon sujjested або, можливо, навіть якийсь просторовий індекс (БД або http://pypi.python.org/pypi/Rtree/ ).


-1

Я використовую словники як своєрідну "базу даних", тому мені потрібно знайти ключ, який я можу повторно використовувати. У моєму випадку, якщо значення ключа є None, то я можу взяти його і повторно використовувати його, не потребуючи "виділення" іншого ідентифікатора. Просто подумав, я поділюсь цим.

db = {0:[], 1:[], ..., 5:None, 11:None, 19:[], ...}

keys_to_reallocate = [None]
allocate.extend(i for i in db.iterkeys() if db[i] is None)
free_id = keys_to_reallocate[-1]

Мені це подобається, тому що мені не потрібно намагатися вловлювати будь-які помилки, такі як StopIterationабо IndexError. Якщо є ключ, то він free_idбуде містити його. Якщо цього немає, то це просто буде None. Напевно, не пітонічно, але я дуже не хотів tryтут використовувати тут ...

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.