Ricerca nel dizionario inverso in Python


102

Esiste un modo semplice per trovare una chiave conoscendo il valore all'interno di un dizionario?

Tutto quello a cui riesco a pensare è questo:

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


dai un'occhiata alla mia risposta su come costruire un dizionario invertito
Salvador Dali

Google mi ha guidato qui ... E devo dire ... perché nessuno lo usa iteritemscome per me questo fa una differenza 40 volte più veloce ... usando il metodo () .next
Angry 84

4
Se hai molte ricerche inverse da fare:reverse_dictionary = {v:k for k,v in dictionary.items()}
Austin

Risposte:


5

Non c'è nessuno. Non dimenticare che il valore può essere trovato su un numero qualsiasi di chiavi, incluso 0 o più di 1.


2
python ha un metodo .index sugli elenchi che restituisce il primo indice trovato con il valore specificato o un'eccezione se non trovato ... qualche motivo per cui una tale semantica non può essere applicata ai dizionari?
Brian Jack

@BrianJack: i dizionari non sono ordinati, come i set. Guardate collections.OrderedDict per un'implementazione che è ordinato.
Martijn Pieters

3
.index deve solo garantire che restituisca un singolo valore e non è necessario che sia lessicalmente primo, ma solo che sia la prima corrispondenza e che il suo comportamento sia stabile (più chiamate sullo stesso dict nel tempo dovrebbero produrre lo stesso elemento corrispondente). A meno che i dizionari non riorganizzino i loro hash non modificati nel tempo man mano che altri elementi vengono aggiunti, rimossi o modificati, funzionerebbe comunque adeguatamente. Un'implementazione ingenua: dictObject.items (). Index (chiave)
Brian Jack

il punto principale di .index () è che per definizione non ci interessano i duplicati solo che possiamo cercare un singolo elemento in modo coerente
Brian Jack

130
Detesto le non risposte come questa. "Smettila di provare a fare quello che giustamente vuoi fare!" non è una risposta accettabile. Perché è stato accettato? Come attestano le risposte più quotate a questa domanda, la ricerca nel dizionario inverso è banalmente implementabile in meno di 80 caratteri di puro Python. Non c'è niente di più "diretto" di così. Paul McGuire 's soluzione è probabilmente il più efficiente, ma tutto il lavoro. </sigh>
Cecil Curry

95

La tua comprensione dell'elenco passa attraverso tutti gli elementi del dict trovando tutte le corrispondenze, quindi restituisce solo la prima chiave. Questa espressione del generatore itererà solo nella misura necessaria per restituire il primo valore:

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

dov'è ddil dict. Solleverà StopIterationse non viene trovata alcuna corrispondenza, quindi potresti volerlo prendere e restituire un'eccezione più appropriata come ValueErroro KeyError.


1
Sì Dovrebbe probabilmente sollevare la stessa eccezione di listObject.index (chiave) quando la chiave non è nell'elenco.
Brian Jack

7
anche keys = { key for key,value in dd.items() if value=='value' }per ottenere il set di tutte le chiavi se più corrispondenze.
askewchan

6
@askewchan - non c'è davvero bisogno di restituirlo come un set, le chiavi di comando devono già essere univoche, basta restituire un elenco - o meglio, restituire un'espressione del generatore e lasciare che il chiamante la metta in qualsiasi contenitore desideri.
PaulMcG

55

Ci sono casi in cui un dizionario è uno: una mappatura

Per esempio,

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

Il tuo approccio va bene se stai facendo una sola ricerca. Tuttavia, se è necessario eseguire più di una ricerca, sarà più efficiente creare un dizionario inverso

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

Se esiste la possibilità di più chiavi con lo stesso valore, sarà necessario specificare il comportamento desiderato in questo caso.

Se il tuo Python è 2.6 o precedente, puoi usare

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

6
Bella ottimizzazione. Ma penso che volevi trasformare la tua lista di 2 tuple in un dizionario usando dict ():ivd=dict([(v,k) for (k,v) in d.items()])
piani cottura

4
@hobs usa semplicemente una comprensione dei invd = { v:k for k,v in d.items() }
dettami

Le comprensioni di dict @gnibbler non sono state migrate di nuovo a Python 2.6, quindi se vuoi rimanere portabile dovrai sopportare i 6 caratteri extra per dict () attorno a un generatore di 2-tuple o una comprensione di lista di 2 -tuple
piani cottura

@hobs, l'ho aggiunto alla mia risposta.
John La Rooy

32

Questa versione è più corta del 26% rispetto alla tua ma funziona in modo identico, anche per valori ridondanti / ambigui (restituisce la prima corrispondenza, come fa la tua). Tuttavia, probabilmente è due volte più lento del tuo, perché crea due volte un elenco dal dict.

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

Oppure, se preferisci la brevità alla leggibilità, puoi salvare un altro carattere con

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

E se preferisci l'efficienza, l' approccio di @ PaulMcGuire è migliore. Se ci sono molte chiavi che condividono lo stesso valore è più efficiente non istanziare quell'elenco di chiavi con una comprensione dell'elenco e utilizzare invece un generatore:

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

2
Supponendo un'operazione atomica, è garantito che chiavi e valori siano nello stesso ordine corrispondente?
Noctis Skytower

1
@NoctisSkytower Sì, dict.keys()e dict.values()sono garantiti per corrispondere fintanto che dictnon viene modificato tra le chiamate.
piani cottura

7

Dato che questo è ancora molto rilevante, il primo successo di Google e passo un po 'di tempo a capirlo, posterò la mia soluzione (funzionante in 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'

Ti darà il primo valore che corrisponde.


6

Forse una classe simile a un dizionario come quella DoubleDictin basso è quello che vuoi? È possibile utilizzare una qualsiasi delle metaclassi fornite in combinazione con DoubleDicto evitare di utilizzare qualsiasi metaclasse.

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

No, non puoi farlo in modo efficiente senza guardare in tutte le chiavi e controllare tutti i loro valori. Quindi avrai bisogno di O(n)tempo per farlo. Se hai bisogno di fare molte di queste ricerche, dovrai farlo in modo efficiente costruendo un dizionario invertito (può essere fatto anche in O(n)) e poi facendo una ricerca all'interno di questo dizionario invertito (ogni ricerca richiederà in media O(1)).

Ecco un esempio di come costruire un dizionario invertito (che sarà in grado di eseguire una mappatura da uno a molti) da un dizionario normale:

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)

Ad esempio se il tuo

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

la tua h_reversedsarà

{
  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

Non ce n'è uno per quanto ne so, un modo per farlo è creare un dettato per la ricerca normale per chiave e un altro per la ricerca inversa per valore.

C'è un esempio di tale implementazione qui:

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

Ciò significa che la ricerca delle chiavi per un valore potrebbe produrre più risultati che possono essere restituiti come un semplice elenco.


Tieni presente che ci sono molti, molti valori possibili che non sono chiavi valide.
Ignacio Vazquez-Abrams

1

So che potrebbe essere considerato "uno spreco", ma in questo scenario spesso memorizzo la chiave come colonna aggiuntiva nel record del valore:

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

è un compromesso e sembra sbagliato, ma è semplice e funziona e ovviamente dipende dal fatto che i valori siano tuple piuttosto che valori semplici.


1

Crea un dizionario inverso

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

Se hai molte ricerche inverse da fare


0

Attraverso i valori nel dizionario possono essere oggetti di qualsiasi tipo non possono essere hash o indicizzati in altro modo. Quindi trovare la chiave in base al valore è innaturale per questo tipo di raccolta. Qualsiasi query del genere può essere eseguita solo in tempo O (n). Quindi, se questo è un compito frequente, dovresti dare un'occhiata all'indicizzazione di chiavi come Jon sujjested o forse anche a qualche indice spaziale (DB o http://pypi.python.org/pypi/Rtree/ ).


-1

Sto usando i dizionari come una sorta di "database", quindi ho bisogno di trovare una chiave che posso riutilizzare. Nel mio caso, se il valore di una chiave è None, posso prenderlo e riutilizzarlo senza dover "allocare" un altro ID. Ho solo pensato di condividerlo.

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]

Mi piace perché non devo cercare di rilevare errori come StopIterationo IndexError. Se è disponibile una chiave, free_idne conterrà una. Se non c'è, allora sarà semplicemente None. Probabilmente non pitonico, ma davvero non volevo usare un tryqui ...

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.