19
\$\begingroup\$

I would like to get a deeply-nested value, for example {"a":{"b":{"c":"myValue"}} by providing the keys to traverse. I tried chaining together .get() but that didn't work for cases where some of the keys were missing—in those cases, I want to get None instead.

Could this solution be improved?

#!/usr/bin/env python
# encoding: utf-8

def nestedGet(d,p):
    if len(p) > 1:
        try:
            return nestedGet(d.get(p[0]),p[1:])
        except AttributeError:
            return None
    if len(p) == 1:
        try:
            return d.get(p[0])
        except AttributeError:
            return None

print nestedGet({"a":{"b":{"c":1}}},["a","b","c"]) #1
print nestedGet({"a":{"bar":{"c":1}}},["a","b","c"]) #None
\$\endgroup\$
1

5 Answers 5

13
\$\begingroup\$

Unless I'm missing something you'd like to implement, I would go for a simple loop instead of using recursion:

def nested_get(input_dict, nested_key):
    internal_dict_value = input_dict
    for k in nested_key:
        internal_dict_value = internal_dict_value.get(k, None)
        if internal_dict_value is None:
            return None
    return internal_dict_value

print(nested_get({"a":{"b":{"c":1}}},["a","b","c"])) #1
print(nested_get({"a":{"bar":{"c":1}}},["a","b","c"])) #None
\$\endgroup\$
9
\$\begingroup\$

I like the following approach that makes use of functools.reduce. It generalizes nicely to other indexable sequences such as lists and tuples.

from functools import reduce
def get_nested_item(data, keys):
    return reduce(lambda seq, key: seq[key], keys, data)

This can be further simplified by direct access to the __getitem__ method of the sequences, which leads to a noticeable increase in performance (I measured 20-35% for different test data):

from functools import reduce
from operator import getitem
def get_nested_item(data, keys):
    return reduce(getitem, keys, data)

Usage:

get_nested_item(data={"a":{"b":{"c":1}}}, keys=["a","b","c"]) # => 1
get_nested_item(data=[[[1,2,3],[10,20,30]]], keys=[0,1,2])    # => 30
get_nested_item(data={'a':0, 'b':[[1,2]]}, keys=['b',0,1])    # => 2
get_nested_item(data=some_sequence, keys=[])                  # => some_sequence

If one prefers soft failure for missing keys/indices, one can catch KeyError or IndexError.

def get_nested_item(data, keys):
    try:
        return reduce(getitem, keys, data)
    except (KeyError, IndexError):
        return None
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Instead of using except (KeyError, IndexError): LookupError could be used. According to the documentation: "The base class for the exceptions that are raised when a key or index used on a mapping or sequence is invalid: IndexError, KeyError." So it would be: except LookupError: \$\endgroup\$
    – Yom
    Oct 7, 2021 at 14:22
6
\$\begingroup\$

If you were to solve it with a third-party package, like jsonpath-rw, the solution would be as simple as constructing the path by joining the keys with a dot and parsing the dictionary:

from jsonpath_rw import parse


def nested_get(d, path):
    result = parse(".".join(path)).find(d)
    return result[0].value if result else None

Some code style notes about your current approach:

  • Python functions does not follow the camel case naming conventions, put an underscore between words - nested_get() instead of nestedGet()
  • there should be extra spaces after the commas (PEP8 reference)
  • improve on variable naming, make them more descriptive - for instance, p can be named as path
  • use print() function instead of a statement for Python3.x compatibility
\$\endgroup\$
6
\$\begingroup\$

You can chain together dict.get() functions and use the optional default argument and specify an empty dict to return instead of None if the key doesn't exist. Except, let the last .get() return None if the key is not found.

If any of the keys to be traversed are missing, a empty dict is returned and the following .get()s in the chain will be called on the empty dict, which is fine., The final .get() in the chain will either return the value if all the keys exist or will return None as desired.

example 1:

input_dict = {"a":{"b":{"c":1}}} #1
input_dict.get("a", {}).get("b", {}).get("c") # 1

example 2:

input_dict = {"a":{"bar":{"c":1}}} #1
input_dict.get("a", {}).get("b", {}).get("c") # None
\$\endgroup\$
2
\$\begingroup\$

Here is a compact and versatile way to accomplish this without extra libraries:

nest = {"a":{"b":{"c":"myValue"}}}

def getElementFromDict(dict, elementChain):
    for key in elementChain:
        dict = dict.get(key, {})
    return dict

getElementFromDict(nest,['a','b','c'])

yields myvalue

\$\endgroup\$
5
  • 3
    \$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$ Feb 23 at 12:54
  • 1
    \$\begingroup\$ @TobySpeight It is very short and consise, but there is very definitely an observation there about verbosity and extra libraries. \$\endgroup\$
    – pacmaninbw
    Feb 23 at 14:46
  • 1
    \$\begingroup\$ I don't think so - the question code includes no libraries that aren't in this implementation. It's just a code dump with some text added as a kind of justification. \$\endgroup\$ Feb 23 at 14:51
  • \$\begingroup\$ @TobySpeight I reviewed the code and I'm actually using it as suggested here. Please read my entry statement and compare it to the other solutions. \$\endgroup\$
    – Fred
    Feb 23 at 19:54
  • 1
    \$\begingroup\$ Bear in mind that most of the other answers are very old, and not up to the standards expected of an answer in 2022. \$\endgroup\$ Feb 23 at 20:26

Not the answer you're looking for? Browse other questions tagged or ask your own question.