I have a nested dictionary. Is there only one way to get values out safely?
try:
example_dict['key1']['key2']
except KeyError:
pass
Or maybe python has a method like get()
for nested dictionary ?
I have a nested dictionary. Is there only one way to get values out safely?
try:
example_dict['key1']['key2']
except KeyError:
pass
Or maybe python has a method like get()
for nested dictionary ?
You could use get
twice:
example_dict.get('key1', {}).get('key2')
This will return None
if either key1
or key2
does not exist.
Note that this could still raise an AttributeError
if example_dict['key1']
exists but is not a dict (or a dict-like object with a get
method). The try..except
code you posted would raise a TypeError
instead if example_dict['key1']
is unsubscriptable.
Another difference is that the try...except
short-circuits immediately after the first missing key. The chain of get
calls does not.
If you wish to preserve the syntax, example_dict['key1']['key2']
but do not want it to ever raise KeyErrors, then you could use the Hasher recipe:
class Hasher(dict):
# https://stackoverflow.com/a/3405143/190597
def __missing__(self, key):
value = self[key] = type(self)()
return value
example_dict = Hasher()
print(example_dict['key1'])
# {}
print(example_dict['key1']['key2'])
# {}
print(type(example_dict['key1']['key2']))
# <class '__main__.Hasher'>
Note that this returns an empty Hasher when a key is missing.
Since Hasher
is a subclass of dict
you can use a Hasher in much the same way you could use a dict
. All the same methods and syntax is available, Hashers just treat missing keys differently.
You can convert a regular dict
into a Hasher
like this:
hasher = Hasher(example_dict)
and convert a Hasher
to a regular dict
just as easily:
regular_dict = dict(hasher)
Another alternative is to hide the ugliness in a helper function:
def safeget(dct, *keys):
for key in keys:
try:
dct = dct[key]
except KeyError:
return None
return dct
So the rest of your code can stay relatively readable:
safeget(example_dict, 'key1', 'key2')
You could also use python reduce:
def deep_get(dictionary, *keys):
return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
Building up on Yoav's answer, an even safer approach:
def deep_get(dictionary, *keys):
return reduce(lambda d, key: d.get(key, None) if isinstance(d, dict) else None, keys, dictionary)
By combining all of these answer here and small changes that I made, I think this function would be useful. its safe, quick, easily maintainable.
def deep_get(dictionary, keys, default=None):
return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
Example :
>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
... return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>
While the reduce approach is neat and short, I think a simple loop is easier to grok. I've also included a default parameter.
def deep_get(_dict, keys, default=None):
for key in keys:
if isinstance(_dict, dict):
_dict = _dict.get(key, default)
else:
return default
return _dict
As an exercise to understand how the reduce one-liner worked, I did the following. But ultimately the loop approach seems more intuitive to me.
def deep_get(_dict, keys, default=None):
def _reducer(d, key):
if isinstance(d, dict):
return d.get(key, default)
return default
return reduce(_reducer, keys, _dict)
Usage
nested = {'a': {'b': {'c': 42}}}
print deep_get(nested, ['a', 'b'])
print deep_get(nested, ['a', 'b', 'z', 'z'], default='missing')
A recursive solution. It's not the most efficient but I find it a bit more readable than the other examples and it doesn't rely on functools.
def deep_get(d, keys):
if not keys or d is None:
return d
return deep_get(d.get(keys[0]), keys[1:])
Example
d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code']) # => 200
deep_get(d, ['garbage', 'status_code']) # => None
A more polished version
def deep_get(d, keys, default=None):
"""
Example:
d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code']) # => 200
deep_get(d, ['garbage', 'status_code']) # => None
deep_get(d, ['meta', 'garbage'], default='-') # => '-'
"""
assert type(keys) is list
if d is None:
return default
if not keys:
return d
return deep_get(d.get(keys[0]), keys[1:], default)
for a second level key retrieving, you can do this:
key2_value = (example_dict.get('key1') or {}).get('key2')
A simple class that can wrap a dict, and retrieve based on a key:
class FindKey(dict):
def get(self, path, default=None):
keys = path.split(".")
val = None
for key in keys:
if val:
if isinstance(val, list):
val = [v.get(key, default) if v else None for v in val]
else:
val = val.get(key, default)
else:
val = dict.get(self, key, default)
if not val:
break
return val
For example:
person = {'person':{'name':{'first':'John'}}}
FindDict(person).get('person.name.first') # == 'John'
If the key doesn't exist, it returns None
by default. You can override that using a default=
key in the FindDict
wrapper -- for example`:
FindDict(person, default='').get('person.name.last') # == doesn't exist, so ''
After seeing this for deeply getting attributes, I made the following to safely get nested dict
values using dot notation. This works for me because my dicts
are deserialized MongoDB objects, so I know the key names don't contain .
s. Also, in my context, I can specify a falsy fallback value (None
) that I don't have in my data, so I can avoid the try/except pattern when calling the function.
from functools import reduce # Python 3
def deepgetitem(obj, item, fallback=None):
"""Steps through an item chain to get the ultimate value.
If ultimate value or path to value does not exist, does not raise
an exception and instead returns `fallback`.
>>> d = {'snl_final': {'about': {'_icsd': {'icsd_id': 1}}}}
>>> deepgetitem(d, 'snl_final.about._icsd.icsd_id')
1
>>> deepgetitem(d, 'snl_final.about._sandbox.sbx_id')
>>>
"""
def getitem(obj, name):
try:
return obj[name]
except (KeyError, TypeError):
return fallback
return reduce(getitem, item.split('.'), obj)
An adaptation of unutbu's answer that I found useful in my own code:
example_dict.setdefaut('key1', {}).get('key2')
It generates a dictionary entry for key1 if it does not have that key already so that you avoid the KeyError. If you want to end up a nested dictionary that includes that key pairing anyway like I did, this seems like the easiest solution.
Since raising an key error if one of keys is missing is a reasonable thing to do, we can even not check for it and get it as single as that:
def get_dict(d, kl):
cur = d[kl[0]]
return get_dict(cur, kl[1:]) if len(kl) > 1 else cur