0

I want to do this:

    foo23 = base["foo23"]["subfoo"]["subsubfoo"]
    print(foo23)
    [2,3]

    foo23 = base["noexist"]["nothereeither"]["nope"]
    print(foo23)
    None

I can't seem to accomplish this, using defaultdict and specialized dictionaries. The failed access of the first call can return a 'None', but then the following fields cause an exception for it not being subscriptable. Just wondering if this is possible.

Jiminion
  • 5,080
  • 1
  • 31
  • 54
  • 1
    Are the levels given? Or can the nesting depth be arbitrary? – user2390182 Aug 18 '23 at 14:05
  • Yes, arbitrary number of levels. If it is 12 levels and valid, it should return the valid value or list/tuple. If not, it should return a None. (I added the list return because if only values are returned, it might be slightly easier to accomplish, but not what is needed.) – Jiminion Aug 18 '23 at 14:08
  • 1
    You want to check out the `get()` method. `foo23 = base.get("noexist", {}).get("nothereeither", {}).get("nope")` – JonSG Aug 18 '23 at 14:09
  • Does this answer your question? [Why dict.get(key) instead of dict\[key\]?](https://stackoverflow.com/questions/11041405/why-dict-getkey-instead-of-dictkey) – JonSG Aug 18 '23 at 14:09
  • @JonSG Interesting. That is a bit cumbersome. We can check the individual levels now (before checking further) and this seems like a variation on that. – Jiminion Aug 18 '23 at 14:13
  • 1
    Sure, if you want to pass a list of keys to a method and have it walk the structure you can do that as well. The implementation is likely to use `in` and/or `get()` though. Perhaps via one of these strategies [Access nested dictionary items via a list of keys](https://stackoverflow.com/questions/14692690/access-nested-dictionary-items-via-a-list-of-keys) – JonSG Aug 18 '23 at 14:17

2 Answers2

2

The elegant random depth way to do this:

tree = lambda: defaultdict(tree)

base = tree()
base["noexist"]["nothereeither"]["nope"]

Now, this returns an empty defaultdict which you would have to handle, e.g.:

print(base["noexist"]["nothereeither"]["nope"] or None)

The less pretty, but more to the point, special variant for exactly 3 nesting levels:

deep3 = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: None)))
print(deep3["noexist"]["nothereeither"]["nope"])
# None

That being said the cleanest would be to access your dict with a special function:

def access(obj, keys, default=None):
    if not keys:
        return obj
    head, *tail = keys
    if head not in obj:
        return default
    return access(obj[head], tail, default=default)

print(access(base, ["foo23", "subfoo", "subsubfoo"]))
# None
user2390182
  • 72,016
  • 6
  • 67
  • 89
1

If you wanted to walk a tree by a list of keys where the tree might have nodes that where dictionaries or lists or values then you might do:

def find_value_by_keys(root, list_of_keys):
    for key in list_of_keys:
        try:
            root = root[key]
        except KeyError:
            return None
    return root

root = {
    "foo": {"bar": "baz"},
    "foo2": ["bar", "baz"]
}
print(find_value_by_keys(root, ["foo", "bar"]))
print(find_value_by_keys(root, ["foo2", 1]))
print(find_value_by_keys(root, ["foo", "noexist"]))
print(find_value_by_keys(root, ["foo", 2]))

That will give you:

baz
baz
None
None

while failing fast and without resorting to a casting your tree to a default dict.

If you wanted to support a list of keys that was a dot separated sting you might be able to do that if you guarded for only passing integer indexes to lists. Perhaps something like:

def find_value_by_keys(root, list_of_keys):
    if isinstance(list_of_keys, str):
        list_of_keys = list_of_keys.split(".")

    for key in list_of_keys:
        try:
            root = root[int(key) if isinstance(root, list) else key]
        except (ValueError, KeyError):
            return None
    return root

root = {
    "foo": {"bar": "baz"},
    "foo2": ["bar", "baz"]
}
print(find_value_by_keys(root, ["foo", "bar"]))
print(find_value_by_keys(root, "foo.bar"))

print(find_value_by_keys(root, ["foo2", 1]))
print(find_value_by_keys(root, "foo2.1"))
print(find_value_by_keys(root, "foo2.x"))

print(find_value_by_keys(root, ["foo", "noexist"]))
print(find_value_by_keys(root, "foo.noexist"))

print(find_value_by_keys(root, ["foo", 2]))
print(find_value_by_keys(root, "foo.2"))
JonSG
  • 10,542
  • 2
  • 25
  • 36
  • 1
    We are thinking about something like this. find_value(root, "this.this.this"). Since this is a 'wrapper', does that make it less Pythonic? – Jiminion Aug 18 '23 at 14:47
  • 1
    I think it is fine. you can just split that key list `for key in list_of_keys.split(".")` – JonSG Aug 18 '23 at 14:48
  • 1
    The only rub I see is that the list keys would be strings and list support would need some attention. You also can't just cast the numeric keys to integers as that might upset dictionary keys that were string numbers. Let me give you a version that might support this – JonSG Aug 18 '23 at 14:55