17

Given the following dictionary:

d = {"a":{"b":{"c":"winning!"}}}

I have this string (from an external source, and I can't change this metaphor).

k = "a.b.c"

I need to determine if the dictionary has the key 'c', so I can add it if it doesn't.

This works swimmingly for retrieving a dot notation value:

reduce(dict.get, key.split("."), d)

but I can't figure out how to 'reduce' a has_key check or anything like that.

My ultimate problem is this: given "a.b.c.d.e", I need to create all the elements necessary in the dictionary, but not stomp them if they already exist.

martineau
  • 119,623
  • 25
  • 170
  • 301
hikaru
  • 2,444
  • 4
  • 22
  • 29
  • 4
    Yes, just accept your own answers to your questions. That's perfectly OK (even desired, so people who have the same question will see that there exists an answer). – Tim Pietzcker Sep 13 '12 at 21:22
  • By any chance is the source actually Cocoa KVC? And, if so, can you just use PyObjC instead of pure Python? (Besides being simpler, that would also guarantee that the keys mean exactly the same thing they're intended to, even if there are bugs in other people's code…) – abarnert Sep 13 '12 at 21:53
  • What is so special in using dot notation 'as a string' to search through dictionary? One has to use some additional function for it. What about to search multi-dimentional dicts JS style, without any strings `x = conf.a.b.c`. I don't know if it is possible in Python. But using a string for it is not cool – Green Feb 13 '13 at 16:53
  • That 'reduce' fragment helped me. Thanks! – Alain Collins Apr 21 '15 at 19:52
  • Near-duplicate, although from 2018 and presumably you asked about Python 2.x not 3.x [Set Python dict items recursively, when given a compound key 'foo.bar.baz'](https://stackoverflow.com/questions/48729220/set-python-dict-items-recursively-when-given-a-compound-key-foo-bar-baz) – smci Jan 06 '19 at 10:03
  • But in this case, you want something in between a `get()` method that also does `set()` on the missing levels. And thus will never return KeyError for missing keys. And when you say "I need to determine if the dictionary has the key 'c'", is that just as a building-block for the answer, or you actually want a recursive `has_key`? I think you're asking for the former, if the latter needs editing. By the way `reduce` went away in Python 3. Should we tag this [tag:python-2.x], or could you please fix the question? – smci Jan 08 '19 at 09:38

5 Answers5

33

You could use an infinite, nested defaultdict:

>>> from collections import defaultdict
>>> infinitedict = lambda: defaultdict(infinitedict)
>>> d = infinitedict()
>>> d['key1']['key2']['key3']['key4']['key5'] = 'test'
>>> d['key1']['key2']['key3']['key4']['key5']
'test'

Given your dotted string, here's what you can do:

>>> import operator
>>> keys = "a.b.c".split(".")
>>> lastplace = reduce(operator.getitem, keys[:-1], d)
>>> lastplace.has_key(keys[-1])
False

You can set a value:

>>> lastplace[keys[-1]] = "something"
>>> reduce(operator.getitem, keys, d)
'something'
>>> d['a']['b']['c']
'something'
jterrace
  • 64,866
  • 22
  • 157
  • 202
18

... or using recursion:

def put(d, keys, item):
    if "." in keys:
        key, rest = keys.split(".", 1)
        if key not in d:
            d[key] = {}
        put(d[key], rest, item)
    else:
        d[keys] = item

def get(d, keys):
    if "." in keys:
        key, rest = keys.split(".", 1)
        return get(d[key], rest)
    else:
        return d[keys]
tobias_k
  • 81,265
  • 12
  • 120
  • 179
  • 1
    This could be improved by calling split only once and passing the resulting list to the recursive function. – l4mpi Sep 13 '12 at 21:51
  • 1
    This one *almost* has it. Problem is, put doesn't account for if a key does exist, but is a text value that needs overwriting with a dict. – hikaru Sep 13 '12 at 22:22
  • 1
    I used this with modifications. Very similar to @l4mpi's answer. – hikaru Sep 13 '12 at 22:35
  • Thanks, this is the only solution that worked for my use case that I've been struggling with all day! – Ashley Kleynhans Aug 08 '22 at 14:15
4

How about an iterative approach?

def create_keys(d, keys):
    for k in keys.split("."):
        if not k in d: d[k] = {}  #if the key isn't there yet add it to d
        d = d[k]                  #go one level down and repeat

If you need the last key value to map to anything else than a dictionary you could pass the value as an additional argument and set this after the loop:

def create_keys(d, keys, value):
    keys = keys.split(".")
    for k in keys[:-1]:
        if not k in d: d[k] = {}
        d = d[k]            
    d[keys[-1]] = value
l4mpi
  • 5,103
  • 3
  • 34
  • 54
1

I thought this discussion was very useful, but for my purpose to only get a value (not setting it), I ran into issues when a key was not present. So, just to add my flair to the options, you can use reduce in combination of an adjusted dict.get() to accommodate the scenario that the key is not present, and then return None:

from functools import reduce
import re
from typing import Any, Optional

def find_key(dot_notation_path: str, payload: dict) -> Any:
    """Try to get a deep value from a dict based on a dot-notation"""

    def get_despite_none(payload: Optional[dict], key: str) -> Any:
        """Try to get value from dict, even if dict is None"""
        if not payload or not isinstance(payload, (dict, list)):
            return None
        # can also access lists if needed, e.g., if key is '[1]'
        if (num_key := re.match(r"^\[(\d+)\]$", key)) is not None:
            try:
                return payload[int(num_key.group(1))]
            except IndexError:
                return None
        else:
            return payload.get(key, None)

    found = reduce(get_despite_none, dot_notation_path.split("."), payload)
   
    # compare to None, as the key could exist and be empty
    if found is None:
        raise KeyError()
    return found

In my use case, I need to find a key within an HTTP request payload, which can often include lists as well. The following examples work:

payload = {
    "haystack1": {
        "haystack2": {
            "haystack3": None, 
            "haystack4": "needle"
        }
    },
    "haystack5": [
        {"haystack6": None}, 
        {"haystack7": "needle"}
    ],
    "haystack8": {},
}

find_key("haystack1.haystack2.haystack4", payload)
# "needle"
find_key("haystack5.[1].haystack7", payload)
# "needle"
find_key("[0].haystack5.[1].haystack7", [payload, None])
# "needle"
find_key("haystack8", payload)
# {}
find_key("haystack1.haystack2.haystack4.haystack99", payload)
# KeyError

EDIT: added list accessor

davidverweij
  • 328
  • 1
  • 15
0
d = {"a":{}}
k = "a.b.c".split(".")

def f(d, i):
    if i >= len(k):
        return "winning!"
    c = k[i]
    d[c] = f(d.get(c, {}), i + 1)
    return d

print f(d, 0)
"{'a': {'b': {'c': 'winning!'}}}"
Rusty Rob
  • 16,489
  • 8
  • 100
  • 116