Python中的反向字典查找

Inverse dictionary lookup in Python

有没有直接的方法通过知道字典中的值来找到一个键?

我能想到的就是:

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


你的列表理解通过所有的dict项找到所有匹配项,然后返回第一个键。此生成器表达式将仅在必要时迭代以返回第一个值:

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

如果dd是听写词,如果找不到匹配项,则会提高StopIteration,因此您可能希望抓住它并返回一个更合适的异常,如ValueErrorKeyError


有些情况下字典是一个:一个映射

如,

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

如果您只进行一次查找,那么您的方法是可以的。但是,如果需要进行多个查找,则创建一个反向字典会更有效。

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

如果有可能有多个具有相同值的键,则在这种情况下需要指定所需的行为。

如果您的python是2.6或更高版本,则可以使用

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


此版本比您的版本短26%,但功能相同,即使是对于冗余/不明确的值(返回第一个匹配项,与您的匹配项相同)。但是,它的速度可能是您的慢一倍,因为它从dict中创建了两次列表。

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

或者,如果您喜欢简洁而不是易读性,您可以使用

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

如果你更喜欢效率的话,@paulmcguire的方法更好。如果有许多键共享相同的值,则不通过列表理解来实例化键列表更有效,而是使用生成器:

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


没有。不要忘记,可以在任意数量的键上找到该值,包括0或大于1。


由于这仍然是非常相关的,第一次谷歌点击,我只是花了一些时间来解决这个问题,我将发布我的(在Python3中工作)解决方案:

1
2
3
4
5
6
7
8
9
10
11
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'

它将为您提供匹配的第一个值。


也许你想要一个类似字典的类,比如下面的DoubleDict?您可以将所提供的任何一个元类与DoubleDict结合使用,也可以完全避免使用任何元类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
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))


不,如果不查找所有键并检查它们的所有值,就无法有效地执行此操作。所以你需要O(n)时间来完成这项工作。如果你需要做很多这样的查找,你需要通过构建一个反向字典(也可以在O(n)中进行),然后在这个反向字典中进行搜索(每个搜索平均需要O(1)进行)。

下面是一个如何从普通字典构造反向字典(它将能够进行一对多映射)的示例:

1
2
3
4
5
6
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)

例如,如果

1
2
3
4
5
6
7
8
9
10
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
2
3
4
5
6
7
8
9
{
  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])
}

据我所知,目前还没有一种方法,但是一种方法是创建一个按键进行正常查找的dict,另一个按值进行反向查找的dict。

这里有一个这样的实现示例:

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

这意味着查找某个值的键可能会产生多个结果,这些结果可以作为一个简单的列表返回。


创建反向字典。然后进行正常查找。

电话簿反向查找示例:

1
2
3
4
5
6
7
8
9
10
11
normDic = {'KVOTHE':['789-2583']
        ,'DENNA':['987-6453']}

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

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

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

由于dict中的值可能不存在,所以一个更为pythonic和自动文档化的代码将是:

1
2
3
4
5
6
7
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.

事实上,口述并不是用来回答这些问题的,如果你在一个新设计的程序中遇到这个问题,那么你可能应该回顾一下你的设计。


我使用字典作为一种"数据库",所以我需要找到一个可以重用的键。在我的例子中,如果一个键的值是None,那么我可以不需要"分配"另一个ID就可以获取并重用它。我只是想共享它。

1
2
3
4
5
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]

我喜欢这个,因为我不必尝试去捕捉任何错误,如StopIterationIndexError。如果有可用的密钥,那么free_id将包含一个密钥。如果没有,那么它只是None。可能不是Python,但我真的不想在这里使用try


我知道这可能被认为是"浪费",但在这种情况下,我经常将键作为附加列存储在值记录中:

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

这是一种权衡,感觉是错误的,但它很简单,而且有效,当然取决于值是元组而不是简单值。


通过字典中的值可以是任何类型的对象,它们不能以其他方式散列或索引。因此,对于此集合类型,按值查找键是不自然的。任何类似的查询都只能在O(N)时间内执行。因此,如果这是一项频繁的任务,那么您应该寻找一些键的索引,比如jon sujjested,或者甚至一些空间索引(db或http://pypi.python.org/pypi/rtree/)。


1
key in dict.values()

这就是字面意思