Inverse Wörterbuchsuche in Python

102

Gibt es eine einfache Möglichkeit, einen Schlüssel zu finden, indem Sie den Wert in einem Wörterbuch kennen?

Ich kann mir nur Folgendes vorstellen:

key = [key for key, value in dict_obj.items() if value == 'value'][0]
RadiantHex
quelle
Mögliches Duplikat: stackoverflow.com/questions/483666/…
Tobias Kienzler
Schauen Sie sich meine Antwort an, wie man ein umgekehrtes Wörterbuch erstellt
Salvador Dali
Google hat mich hierher geführt ... Und ich muss sagen ... warum verwendet niemand, iteritemsda dies für mich einen 40-mal schnelleren Unterschied macht ... mit der () .next-Methode
Angry 84
4
Wenn Sie viele Reverse-Lookups durchführen müssen:reverse_dictionary = {v:k for k,v in dictionary.items()}
Austin

Antworten:

6

Da ist gar nichts. Vergessen Sie nicht, dass der Wert auf einer beliebigen Anzahl von Schlüsseln gefunden werden kann, einschließlich 0 oder mehr als 1.

Ignacio Vazquez-Abrams
quelle
2
Python hat eine .index-Methode, um den ersten gefundenen Index mit dem angegebenen Wert oder einer Ausnahme zurückzugeben, wenn er nicht gefunden wird. Gibt es einen Grund, warum eine solche Semantik nicht auf Wörterbücher angewendet werden konnte?
Brian Jack
@BrianJack: Wörterbücher werden nicht wie Sets bestellt. Schauen Sie sich collections.OrderedDict für eine Implementierung , die wird bestellt.
Martijn Pieters
3
.index muss nur sicherstellen, dass es einen einzelnen Wert zurückgibt, und es muss nicht lexikalisch zuerst sein, nur dass es die erste Übereinstimmung ist und dass sein Verhalten stabil ist (mehrere Aufrufe mit demselben Diktat über die Zeit sollten dasselbe Übereinstimmungselement ergeben). Wenn Wörterbücher ihre unveränderten Hashes nicht im Laufe der Zeit neu anordnen, wenn andere Elemente hinzugefügt, entfernt oder geändert werden, funktioniert dies weiterhin ordnungsgemäß. Eine naive Implementierung: dictObject.items (). Index (Schlüssel)
Brian Jack
Der Hauptpunkt von .index () ist, dass wir uns per Definition nicht nur um Duplikate kümmern, sondern dass wir ein einzelnes Element konsistent nachschlagen können
Brian Jack,
130
Ich verabscheue solche Nichtantworten. "Hör auf zu versuchen, das zu tun, was du zu Recht willst!" ist keine akzeptable Antwort. Warum wurde das akzeptiert? Wie höher bewertete Antworten auf diese Frage bestätigen, kann die Reverse-Dictionary-Suche in weniger als 80 Zeichen von Pure-Python trivial implementiert werden. "Einfacher" geht es nicht. Die Lösung von Paul McGuire ist wahrscheinlich die effizienteste, aber sie funktionieren alle . </sigh>
Cecil Curry
95

Ihr Listenverständnis durchläuft alle Elemente des Diktats, um alle Übereinstimmungen zu finden, und gibt dann nur den ersten Schlüssel zurück. Dieser Generatorausdruck iteriert nur so weit wie nötig, um den ersten Wert zurückzugeben:

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

Wo ddist das Diktat? Wird erhöht, StopIterationwenn keine Übereinstimmung gefunden wird. Vielleicht möchten Sie diese abfangen und eine passendere Ausnahme wie ValueErroroder zurückgeben KeyError.

PaulMcG
quelle
1
Ja Es sollte wahrscheinlich dieselbe Ausnahme wie listObject.index (Schlüssel) auslösen, wenn der Schlüssel nicht in der Liste enthalten ist.
Brian Jack
7
auch keys = { key for key,value in dd.items() if value=='value' }, um den Satz aller Schlüssel zu erhalten, wenn mehrere übereinstimmen.
Askewchan
6
@askewchan - keine wirkliche Notwendigkeit, dies als Satz zurückzugeben, Diktierschlüssel müssen bereits eindeutig sein, geben Sie einfach eine Liste zurück - oder besser, geben Sie einen Generatorausdruck zurück und lassen Sie den Aufrufer ihn in den gewünschten Container legen.
PaulMcG
56

Es gibt Fälle, in denen ein Wörterbuch eine Eins-zu-Eins-Zuordnung ist

Z.B,

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

Ihr Ansatz ist in Ordnung, wenn Sie nur eine einzige Suche durchführen. Wenn Sie jedoch mehr als eine Suche durchführen müssen, ist es effizienter, ein inverses Wörterbuch zu erstellen

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

Wenn die Möglichkeit besteht, dass mehrere Schlüssel denselben Wert haben, müssen Sie in diesem Fall das gewünschte Verhalten angeben.

Wenn Ihr Python 2.6 oder älter ist, können Sie verwenden

ivd = dict((v, k) for k, v in d.items())
John La Rooy
quelle
6
Schöne Optimierung. Aber ich denke, Sie ivd=dict([(v,k) for (k,v) in d.items()])
Kochfelder
4
@ Hobs verwenden nur ein Diktat Verständnis anstelle von Listenverständnis:invd = { v:k for k,v in d.items() }
Askewchan
@gnibbler Diktatverständnisse wurden nicht zurück auf Python 2.6 migriert. Wenn Sie also portabel bleiben möchten, müssen Sie die 6 zusätzlichen Zeichen für dict () um einen Generator von 2 Tupeln oder ein Listenverständnis von 2 herum ertragen -Tupel
Kochfelder
@hobs, das habe ich meiner Antwort hinzugefügt.
John La Rooy
32

Diese Version ist 26% kürzer als Ihre , funktioniert jedoch auch bei redundanten / mehrdeutigen Werten identisch (gibt die erste Übereinstimmung zurück, wie Ihre). Es ist jedoch wahrscheinlich doppelt so langsam wie deins, da es zweimal eine Liste aus dem Diktat erstellt.

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

Oder wenn Sie die Kürze der Lesbarkeit vorziehen, können Sie ein weiteres Zeichen mit speichern

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

Und wenn Sie Effizienz bevorzugen, ist der Ansatz von @ PaulMcGuire besser. Wenn es viele Schlüssel gibt, die denselben Wert haben, ist es effizienter, diese Liste von Schlüsseln nicht mit einem Listenverständnis zu instanziieren und stattdessen einen Generator zu verwenden:

key = (key for key, value in dict_obj.items() if value == 'value').next()
Kochfelder
quelle
2
Sind Schlüssel und Werte unter der Annahme einer atomaren Operation garantiert in derselben entsprechenden Reihenfolge?
Noctis Skytower
1
@NoctisSkytower Ja, dict.keys()und dict.values()es wird garantiert, dass sie korrespondieren, solange das dictnicht zwischen Anrufen mutiert ist.
Kochfelder
7

Da dies immer noch sehr relevant ist, der erste Google-Treffer und ich nur einige Zeit damit verbringen, dies herauszufinden, werde ich meine (in Python 3 arbeitende) Lösung veröffentlichen:

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'

Sie erhalten den ersten übereinstimmenden Wert.

Freek
quelle
6

Vielleicht ist eine wörterbuchähnliche Klasse wie DoubleDictunten das, was Sie wollen? Sie können jede der bereitgestellten Metaklassen in Verbindung mit einer Metaklasse verwenden DoubleDictoder diese überhaupt vermeiden.

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))
Noctis Skytower
quelle
4

Nein, Sie können dies nicht effizient tun, ohne alle Schlüssel einzusehen und alle ihre Werte zu überprüfen. O(n)Dafür brauchen Sie also Zeit. Wenn Sie viele solcher Suchvorgänge durchführen müssen, müssen Sie dies effizient tun, indem Sie ein umgekehrtes Wörterbuch erstellen (kann auch in ausgeführt werden O(n)) und dann eine Suche innerhalb dieses umgekehrten Wörterbuchs durchführen (jede Suche dauert durchschnittlich O(1)).

Hier ist ein Beispiel für die Erstellung eines umgekehrten Wörterbuchs (das eine bis mehrere Zuordnungen durchführen kann) aus einem normalen Wörterbuch:

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)

Zum Beispiel, wenn Ihr

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])
}

dein h_reversedwird sein

{
  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])
}
Salvador Dali
quelle
2

Soweit ich weiß, gibt es keinen, aber eine Möglichkeit besteht darin, ein Diktat für die normale Suche nach Schlüssel und ein anderes Diktat für die umgekehrte Suche nach Wert zu erstellen.

Hier gibt es ein Beispiel für eine solche Implementierung:

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

Dies bedeutet, dass das Nachschlagen der Schlüssel nach einem Wert zu mehreren Ergebnissen führen kann, die als einfache Liste zurückgegeben werden können.

Jon
quelle
Beachten Sie, dass es viele, viele mögliche Werte gibt, die keine gültigen Schlüssel sind.
Ignacio Vazquez-Abrams
1

Ich weiß, dass dies als "verschwenderisch" angesehen werden kann, aber in diesem Szenario speichere ich den Schlüssel häufig als zusätzliche Spalte im Wertedatensatz:

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

Es ist ein Kompromiss und fühlt sich falsch an, aber es ist einfach und funktioniert und hängt natürlich davon ab, dass Werte eher Tupel als einfache Werte sind.

CarlS
quelle
1

Machen Sie ein umgekehrtes Wörterbuch

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

Wenn Sie viele Reverse-Lookups durchführen müssen

eusoubrasileiro
quelle
0

Durch Werte im Wörterbuch können Objekte jeglicher Art sein, die nicht auf andere Weise gehasht oder indiziert werden können. Daher ist es für diesen Sammlungstyp unnatürlich, den Schlüssel anhand des Werts zu finden. Eine solche Abfrage kann nur in O (n) -Zeit ausgeführt werden. Wenn dies also eine häufige Aufgabe ist, sollten Sie nach einer Indizierung von Schlüsseln wie Jon sujjested oder sogar nach einem räumlichen Index (DB oder http://pypi.python.org/pypi/Rtree/ ) suchen .

Odomontois
quelle
-1

Ich verwende Wörterbücher als eine Art "Datenbank", daher muss ich einen Schlüssel finden, den ich wiederverwenden kann. Wenn in meinem Fall der Wert eines Schlüssels ist None, kann ich ihn nehmen und wiederverwenden, ohne eine andere ID "zuweisen" zu müssen. Ich dachte nur, ich würde es teilen.

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]

Ich mag dieses, weil ich nicht versuchen muss, Fehler wie StopIterationoder abzufangen IndexError. Wenn ein Schlüssel verfügbar ist, free_identhält er einen. Wenn nicht, dann wird es einfach sein None. Wahrscheinlich nicht pythonisch, aber ich wollte tryhier wirklich kein ...

Zizouz212
quelle