I receive a dictionary as input, and want to return a list of keys for which the dictionary values are unique in the scope of that dictionary.
I will clarify with an example. Say my input is dictionary a, constructed as follows:
a = dict()
a['cat'] = 1
a['fish'] = 1
a['dog'] = 2 # <-- unique
a['bat'] = 3
a['aardvark'] = 3
a['snake'] = 4 # <-- unique
a['wallaby'] = 5
a['badger'] = 5
The result I expect is ['dog', 'snake']
.
There are obvious brute force ways to achieve this, however I wondered if there's a neat Pythonian way to get the job done.
9 Answers 9
I think efficient way if dict is too large would be
countMap = {}
for v in a.itervalues():
countMap[v] = countMap.get(v,0) + 1
uni = [ k for k, v in a.iteritems() if countMap[v] == 1]
2 Comments
Note that this actually is a bruteforce:
l = a.values()
b = [x for x in a if l.count(a[x]) == 1]
3 Comments
Here is a solution that only requires traversing the dict once:
def unique_values(d):
seen = {} # dict (value, key)
result = set() # keys with unique values
for k,v in d.iteritems():
if v in seen:
result.discard(seen[v])
else:
seen[v] = k
result.add(k)
return list(result)
2 Comments
result
... docs say """remove(elem) Remove element elem from the set. Raises KeyError if elem is not contained in the set.""">>> b = []
>>> import collections
>>> bag = collections.defaultdict(lambda: 0)
>>> for v in a.itervalues():
... bag[v] += 1
...
>>> b = [k for (k, v) in a.iteritems() if bag[v] == 1]
>>> b.sort() # optional
>>> print b
['dog', 'snake']
>>>
1 Comment
lambda: 0
is more explicit than int
... AFAICT, until defaultdict arrived [2.5], the number of persons who knew that int() produced 0 [since 2.2] instead of an exception was < epsilon and the number who had a use for that knowledge was even smaller :-)A little more verbose, but does need only one pass over a:
revDict = {}
for k, v in a.iteritems():
if v in revDict:
revDict[v] = None
else:
revDict[v] = k
[ x for x in revDict.itervalues() if x != None ]
( I hope it works, since I can't test it here )
2 Comments
x is not None
is preferable to x != None
.What about subclassing?
class UniqueValuesDict(dict):
def __init__(self, *args):
dict.__init__(self, *args)
self._inverse = {}
def __setitem__(self, key, value):
if value in self.values():
if value in self._inverse:
del self._inverse[value]
else:
self._inverse[value] = key
dict.__setitem__(self, key, value)
def unique_values(self):
return self._inverse.values()
a = UniqueValuesDict()
a['cat'] = 1
a['fish'] = 1
a[None] = 1
a['duck'] = 1
a['dog'] = 2 # <-- unique
a['bat'] = 3
a['aardvark'] = 3
a['snake'] = 4 # <-- unique
a['wallaby'] = 5
a['badger'] = 5
assert a.unique_values() == ['dog', 'snake']
2 Comments
del a['bat']; print a.unique_values()
would cause aardvark
to appear in the output but sadly it doesn't and fixing that would require even more convolutions and double__underscores :-(Here's another variation.
>>> import collections
>>> inverse= collections.defaultdict(list)
>>> for k,v in a.items():
... inverse[v].append(k)
...
>>> [ v[0] for v in inverse.values() if len(v) == 1 ]
['dog', 'snake']
I'm partial to this because the inverted dictionary is such a common design pattern.
2 Comments
[v[0] for v in inverse.itervalues() if len(v) == 1
(3) In any case building the whole inverted dict is overkill.You could do something like this (just count the number of occurrences for each value):
def unique(a):
from collections import defaultdict
count = defaultdict(lambda: 0)
for k, v in a.iteritems():
count[v] += 1
for v, c in count.iteritems():
if c <= 1:
yield v
2 Comments
defaultdict(int)
to be a bit more clear than defaultdict(lambda:0)
. Since a default dict of almost any other type will simply use the type name.Use nested list comprehensions!
print [v[0] for v in
dict([(v, [k for k in a.keys() if a[k] == v])
for v in set(a.values())]).values()
if len(v) == 1]
4 Comments
k in a
instead of k in a.keys()
(2) Use whatever.itervalues()
instead of whatever.values()
(3) The dict(yadda yadda) part is building the already-overkill inverse of a
inefficiently (4) It's neither neat nor Python(ic|ian) ... but it's certainly not obvious! (5) Count the number of responders whose first effort at the so-called trivial problem was a stuff-up.solution
can be edited (using only the delete key!) to get rid of building the inverse; still O(N^2) though: print [v[0] for v in [[k for k in a if a[k] == v] for v in set(a.values())] if len(v) == 1]