Kyo
Kyo

Reputation: 27

Check if a key exists in a nested dictionary python based on a list

So I have a dictionary

dict = {"key1" : {"key2": {"key3": 4 } } }

and a list of keys of a hierarchy

list = ['key1","key2","abc"]

Now I want to check if the list of keys exists within the dict retaining that hierarchy, if not maybe return a None. So in the above case I should return a None instead of an Error

The solution must be dynamic for any dict and list of keys for that dict, not a static one involving manually checking it. I've been working on it for a few hours now but couldn't find a solution, any help would be greatly appreciated

Upvotes: 0

Views: 1241

Answers (5)

Dmitry Belaventsev
Dmitry Belaventsev

Reputation: 6657

Answer of Mark Meyer was selected as the right one, but it will fail for the following input:

d = {"key1" : {"key2": {"key3": 4 } }, "key5": {"key6": 1}}
findindict(d, ["key1","key2", "key3", "key6"])
In [86]: def findindict(d, l):
    ...:     try:
    ...:         return reduce(lambda current_dict, key: current_dict[key], l, d)
    ...:     except KeyError:
    ...:         return None
    ...:

In [87]: d = {"key1" : {"key2": {"key3": 4 } }, "key5": {"key6": 1}}

In [88]: findindict(d, ["key1","key2", "key3", "key6"])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-88-42fa73bf849f> in <module>
----> 1 findindict(d, ["key1","key2", "key3", "key6"])

<ipython-input-86-c54e471e6146> in findindict(d, l)
      1 def findindict(d, l):
      2     try:
----> 3         return reduce(lambda current_dict, key: current_dict[key], l, d)
      4     except KeyError:
      5         return None

<ipython-input-86-c54e471e6146> in <lambda>(current_dict, key)
      1 def findindict(d, l):
      2     try:
----> 3         return reduce(lambda current_dict, key: current_dict[key], l, d)
      4     except KeyError:
      5         return None

TypeError: 'int' object is not subscriptable

Catching of that TypeError will not help - it will not traverse all the keys inside nested dict.

It's failing because it will not check all the keys. So let's create the function, which will get all the keys:

def get_all_keys(d):
    dict_keys = list(d.keys()) if isinstance(d, dict) else []
    keys = dict_keys[:]
    for key in dict_keys:
        keys += get_all_keys(d[key])
    return keys

So your target function will look like:

def all_keys_presented_in_dict(d, keys):
    return not bool(set(keys) - set(get_all_keys(d)))

If later you would love to check if at least some (not all) of the keys are presented in target dictionary - you can easily do that, because you have access to all keys in the nested dictionary.


UPDATE

As @MarkMeyer has noticed - OP has asked about "existence" of keys and "retaining of hierarchy". So my solution should be updated to check if one list (of target keys) is subsequence of another list (of all keys, retaining hierarchy).

So let's add the function to check that:

def is_subsequence(subsequence, target):
    subsequence_length = len(subsequence)
    for i in range(len(target) - subsequence_length + 1):
        if target[i:i + subsequence_length] == subsequence:
            return True
    return False

def all_keys_presented_in_dict_with_hierarchy_being_retained(d, keys):
    return is_subsequence(keys, get_all_keys(d))

For example:

In [26]: all_keys_presented_in_dict_with_hierarchy_being_retained(d, ["key2", "key3"])
Out[26]: True

In [27]: all_keys_presented_in_dict_with_hierarchy_being_retained(d, ["key2"])
Out[27]: True

In [28]: all_keys_presented_in_dict_with_hierarchy_being_retained(d, ["key2", "key1"])
Out[28]: False

Upvotes: -1

Mark
Mark

Reputation: 92440

You can use functools.reduce() for this, you just need to anticipate the KeyError. For example:

from functools import reduce

d = {"key1" : {"key2": {"key3": 4 } } }

def findindict(d, l):
    try:
        return reduce(lambda current_dict, key: current_dict[key], l, d)
    except (KeyError, TypeError):
        return None

findindict(d, ["key1","key2", "key3"])
# 4
findindict(d, ["key1","key2", "abc"])
# None
findindict(d, ["key1","key2", "key3", "key6"])
#None

Upvotes: 1

Attila
Attila

Reputation: 66

Don't use dict and list as variable name. If you want to return the structure from the given key:

def NextedKeyExists(dictobj, key):
    if key in dictobj:
        return dictobj[key]
    
    for v in dictobj.values():
        if isinstance(v, dict):
            r = NextedKeyExists(v, key)
            if r != None:
                return r
                
    return None

usage:

dictobj = {"key1" : {"key2": {"key3": 4 } } }
listobj = ["key1", "key2", "abc"]

for l in listobj:
    print (l, " -> ", NextedKeyExists(dictobj, l))

outputs:

key1  ->  {'key2': {'key3': 4}}
key2  ->  {'key3': 4}
abc  ->  None

Upvotes: 1

Rauwuckl
Rauwuckl

Reputation: 21

I think this little recursive function should do it.

def find_path(nested_structure, path):
    top = path[0]
    if top not in nested_structure.keys():
        return None
    else:
        value = nested_structure[top]
        if isinstance(value, dict) and len(path)>0:
            return find_path(nested_structure[top], path[1:])
        else:
            return value

structure = {"key1" : {"key2": {"key3": 4 }, "key2b": 42 } }
     
print(find_path(structure, ["key1","key2","abc"])) # -> None
print(find_path(structure, ["key1","key2","key3"])) # -> 4

Upvotes: 2

wasif
wasif

Reputation: 15488

You can try like this don't use dict as variable name:

mydict = {"key1" : {"key2": {"key3": 4 } } }
mykeys = []
def walk_dict(d,depth=0):
    for k,v in sorted(d.items(),key=lambda x: x[0]):
        if isinstance(v, dict):
            mykeys.append(k)
            walk_dict(v,depth+1)
        else:
            mykeys.append(k)

def rec_check_keys(keylist):
  thekeys = walk_dict(mydict)
  flag = True
  for x in keylist:
    if not x in thekeys:
      flag = False
  if flag == False:
    return None
   else:
     return True

Upvotes: 0

Related Questions