0

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

Kyo
  • 27
  • 4
  • 1
    What have you tried so far? Any ideas how to do that? – Dmitry Belaventsev Oct 12 '20 at 16:41
  • It’s not really clear what you mean by, “retaining that hierarchy”. – Mark Oct 12 '20 at 16:43
  • @DmitryBelaventsev At first I thought of using the reduce() from itertools while passing a lambda of key, value with the key being from the list and value from a dict. However that caused me to face an error when the key from the list doesn't exist in the dict. @ MarkMeyer By retaining hierarchy I mean, it should follow that nested structure (sorry I'm still learning) – Kyo Oct 12 '20 at 16:45
  • 1
    Does this answer your question? [Elegant way to check if a nested key exists in a dict?](https://stackoverflow.com/questions/43491287/elegant-way-to-check-if-a-nested-key-exists-in-a-dict) – Carlo Zanocco Oct 12 '20 at 16:49
  • I've just added the correct answer for you – Dmitry Belaventsev Oct 13 '20 at 09:32

5 Answers5

2

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
Rauwuckl
  • 21
  • 3
1

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
Mark
  • 90,562
  • 7
  • 108
  • 148
  • Your solution will fail for `d = {"key1" : {"key2": {"key3": 4 } }, "key5": {"key6": 1}}` and `findindict(d, ["key1","key2", "key3", "key6"])` – Dmitry Belaventsev Oct 13 '20 at 07:46
  • @DmitryBelaventsev, yes if that's possible you simply need to catch the resulting TypeError. See edit. – Mark Oct 13 '20 at 14:54
  • Thanks for the clarification about question subject and updates in your answer. But still `print(findindict(d, ["key2", "key3"]))` gives `None`, which is incorrect. List of keys exists ("key2" and "key3" are presented in the dict). Hierarchy is retained ("key3" goes as the first key inside "key2"). – Dmitry Belaventsev Oct 13 '20 at 15:57
  • No, it's not incorrect. The OP whats to know if the array defines a path exists in the dictionary. There is no `d["key2"]["key3"]` so it should return false/none. – Mark Oct 13 '20 at 16:11
  • Ok it's up to OP to decide, but there's nothing said about "path". It's about "exists" and "hierarchy" - which is true for key2->key3. – Dmitry Belaventsev Oct 13 '20 at 16:15
  • who knows, maybe he just checked on the sample input :D our goal here is to help community from different angles - so anyone else can see different kind of solutions in one relevant place. – Dmitry Belaventsev Oct 13 '20 at 16:31
1

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
Attila
  • 66
  • 4
0

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
Wasif
  • 14,755
  • 3
  • 14
  • 34
-1

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
Dmitry Belaventsev
  • 6,347
  • 12
  • 52
  • 75
  • You misunderstood the question. Your example is incorrect. The input `["key1","key2", "key3", "key6"]` does not maintain the hierarchy demanded by the OP. There **is no** path from `key3` to `key6` in `{"key1" : {"key2": {"key3": 4 } }, "key5": {"key6": 1}}` so it *should* and *does* return `None` once you catch the TypeError. – Mark Oct 13 '20 at 15:06
  • @MarkMeyer thanks for clarification, I really missed that part of requirements. BTW, hierarchy in that context sounds like "order". And AFAIK, insertion order isn't guaranteed for dictionaries in Python before 3.6. So that kind of request looks really strange (without OrderedDict). How do you think? – Dmitry Belaventsev Oct 13 '20 at 16:02
  • In the meantime it's hard to define "order" there. Let's take a look on `{"key1" : {"key2": {"key3": 4 } }, "key5": {"key6": 1}}`. Is the `key1, key2, key3, key5, key6` sequence is about right order? Or we treat dict like a tree data structure and order is defined per-level? So order can be described by `key1, key5, key2, key6, key3` sequence? – Dmitry Belaventsev Oct 13 '20 at 16:05
  • I think you are making it too complicated. The OP is imagining doing something like `d['key1']['key5']['key2']` and asking if that is valid. In this case it is not. While `d['key1']['key2']['key3']` is. – Mark Oct 13 '20 at 16:13