web-dev-qa-db-fra.com

Recherche dans le dictionnaire inverse en Python

Existe-t-il un moyen simple de trouver une clé en connaissant la valeur d'un dictionnaire?

Tout ce que je peux penser, c'est ceci:

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

Il n'y en a pas. N'oubliez pas que la valeur peut être trouvée sur un nombre quelconque de clés, y compris 0 ou plus de 1.

18

La compréhension de votre liste passe en revue tous les éléments du dict, trouve tous les résultats, puis renvoie simplement la première clé. Cette expression génératrice itérera uniquement dans la mesure nécessaire pour renvoyer la première valeur:

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

dd est le dict. Sera levée StopIteration si aucune correspondance n'est trouvée. Vous voudrez peut-être donc l'attraper et renvoyer une exception plus appropriée telle que ValueError ou KeyError.

73
PaulMcG

Il y a des cas où un dictionnaire est un: un mappage

Par exemple,

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

Votre approche est acceptable si vous ne faites qu'une seule recherche. Toutefois, si vous devez effectuer plusieurs recherches, il sera plus efficace de créer un dictionnaire inverse.

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

S'il est possible que plusieurs clés aient la même valeur, vous devrez spécifier le comportement souhaité dans ce cas.

Si votre Python est 2.6 ou plus ancien, vous pouvez utiliser

ivd = dict((v, k) for k, v in d.items())
44
John La Rooy

Cette version est 26% plus courte que yours / mais fonctionne de manière identique, même pour les valeurs redondantes/ambiguës (renvoie la première correspondance, comme la vôtre) Cependant, il est probablement deux fois plus lent que le vôtre, car il crée deux fois une liste à partir du dict.

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

Ou, si vous préférez la brièveté à la lisibilité, vous pouvez enregistrer un caractère supplémentaire avec

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

Et si vous préférez l'efficacité, l'approche de PaulMcGuire est meilleure. S'il y a beaucoup de clés qui partagent la même valeur, il est plus efficace de ne pas instancier cette liste de clés avec une liste de compréhension, mais d'utiliser plutôt un générateur:

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

Etant donné que cela reste très pertinent, le premier hit de Google et je passe juste du temps à comprendre cela, je vais poster ma solution (en 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'

Cela vous donnera la première valeur qui correspond.

5
Freek

Vous voulez peut-être une classe de type dictionnaire, telle que DoubleDict down ci-dessous? Vous pouvez utiliser l’une des métaclasses fournies en conjonction avec DoubleDict ou vous pouvez éviter d’utiliser une métaclasse.

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))
5
Noctis Skytower

À ma connaissance, il n'y en a pas un. Cependant, une façon de le faire est de créer un dict pour la recherche normale par clé et un autre pour la recherche inversée par valeur.

Voici un exemple d'une telle implémentation:

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

Cela signifie que la recherche des clés pour une valeur peut donner plusieurs résultats qui peuvent être renvoyés sous forme de simple liste.

2
Jon

Non, vous ne pouvez pas le faire efficacement sans regarder dans toutes les clés et vérifier toutes leurs valeurs. Donc, vous aurez besoin de O(n) temps pour le faire. Si vous devez effectuer beaucoup de recherches de ce type, vous devrez le faire efficacement en construisant un dictionnaire inversé (peut également être utilisé dans O(n)), puis en effectuant une recherche à l'intérieur de ce dictionnaire inversé (chaque recherche prendra en moyenne O(1)).

Voici un exemple de construction d'un dictionnaire inversé (qui pourra faire un mappage multiple) à partir d'un dictionnaire normal:

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)

Par exemple si votre 

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

votre h_reversed sera

{
  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
Salvador Dali

J'utilise les dictionnaires comme une sorte de "base de données", je dois donc trouver une clé que je puisse réutiliser. Dans mon cas, si la valeur d'une clé est None, je peux la prendre et la réutiliser sans avoir à "affecter" un autre identifiant. J'ai juste pensé que je le partagerais.

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]

J'aime celui-ci parce que je n'ai pas à essayer d'attraper les erreurs telles que StopIteration ou IndexError. Si une clé est disponible, alors free_id en contiendra une. S'il n'y en a pas, alors ce sera simplement None. Probablement pas Pythonic, mais je ne voulais vraiment pas utiliser une try ici ...

0
Zizouz212

Par le biais de valeurs dans le dictionnaire peuvent être des objets de toute nature, ils ne peuvent pas être hachés ou indexés d'une autre manière. Donc, trouver la clé par la valeur n'est pas naturel pour ce type de collection. Toute requête de ce type ne peut être exécutée qu’en O(n). Donc, si cette tâche est fréquente, vous devriez rechercher une indexation de clé telle que Jon sujjested ou même un index spatial (DB ou http://pypi.python.org/pypi/Rtree/ ).

0
Odomontois

Comme la valeur pourrait être inexistante dans dict, un code plus pythonique et auto-documenté serait:

a  # Value to search against
x = None  # Searched key
for k, v in d.items():
    if v == a:
        x = k
        break
x  # Now contains the key or None if not found.

En effet, les logiciels ne sont pas conçus pour répondre à de telles problématiques. Si vous rencontrez ce problème sur un nouveau programme conçu, vous devriez probablement revoir votre conception.

0
Emmanuel

Créez un dictionnaire inversé. Ensuite, faites une recherche normale.

Exemple de recherche inversée dans l'annuaire téléphonique:

normDic = {'KVOTHE':['789-2583']
        ,'DENNA':['987-6453']}

revDic = {'789-2583':['KVOTHE']
        ,'987-6453':['DENNA']}

numInput = str(input('Enter number:\n>>'))

if numInput in revDic:
    print('Contact found:', revDic[numInput], numInput)
0
aused1

Je sais que cela pourrait être considéré comme un «gaspillage», mais dans ce scénario, je stocke souvent la clé en tant que colonne supplémentaire dans l'enregistrement de valeur:

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

c'est un compromis et on se sent mal, mais c'est simple et cela fonctionne et, bien sûr, cela dépend des valeurs qui sont des tuples plutôt que de simples valeurs.

0
CarlS