9

Suppose I have a dictionary that is nested arbitrarily:

d = {
    11: {
        21: {31: 'a', 32: 'b'},
        22: {31: 'a', 34: 'c'},
    },
    12: {
        1: {2: 3}
    }
}

And a list of keys whose position tells me which nested dictionary to look for every key in:

keys = [11, 21, 31]
# keys = [11, 23, 44]

Is there a simple one liner to do this? I've looked at the questions listed below, and they are similar, but not really what I'm looking for. I have also attempted it myself and came up with this:

from functools import reduce

def lookup(d, key):
    return d.get(key, {}) if d and isinstance(d, dict) else None

def fn(keys, d):
    return reduce(lookup, keys, d)

print(fn(keys, d)) # prints 'a'

The problem with this, is that in case of second list of keys (see commented out keys), it continues looking up nested keys further, even though the higher level key wasn't found, and continuing is pointless. How can I stop reduce as soon as I find a final match or fail (one of the questions listed below addresses it, but I can't really apply it in my use case... or can I?)? Any other ideas? Oh and I want to accomplish this using official python libraries only. So no numpy, pandas etc, but functools, itertools are fine

Python: Convert list to dict keys for multidimensional dict with exception handling

Is there a simple one-liner for accessing each element of a nested dictioanry in Python?

Accessing nested values in nested dictionaries in Python 3.3

Using itertools for recursive function application

Stopping a Reduce() operation mid way. Functional way of doing partial running sum

Finding a key recursively in a dictionary

Thanks!

Community
  • 1
  • 1
Bahrom
  • 4,752
  • 32
  • 41
  • not sure i understand what you are trying to do, will this help? `d[keys[0]][keys[1]][keys[2]]` it will find a final match if exist or fail if not – lc123 Apr 12 '16 at 21:32
  • That's what I want, but I don't want to use indices because the list can be of arbitrary length too – Bahrom Apr 12 '16 at 21:34
  • 2
    If you use `dict.__getitem__` as your function for `reduce`, it will raise an exception when either the type is wrong or the key is not found. I think this meets your need to stop early. Just wrap the `reduce` in a `try` block. – aghast Apr 12 '16 at 21:43

3 Answers3

13

You can use functools.reduce():

from functools import reduce # In Python 2, don't import it. (It's a built-in)

print(reduce(dict.get, keys, d))

# 'a'

For the keys you mentioned, it goes like this:

  • call dict.get with d (initial) and the first item of keys (11) to get d[11]
  • call dict.get with the result (a dictionary) and the next item in keys (21) to get {...}[21]
  • call dict.get ...
    ...

until keys is "reduced" to the final value ('a')

Edit: As dict.get results in None if there is no such key, there might be undesired results. If you want to have a KeyError, you can use operator.getitem instead.

zondo
  • 19,901
  • 8
  • 44
  • 83
  • 1
    You might replace the lambda with `dict.__getitem__`, I think. – Norman Apr 12 '16 at 21:41
  • 1
    @Norman I was going to suggest that but wanted to check the method first and don't have python installed on this computer! But yeah, +1 for that suggestion. – Alex Van Liew Apr 12 '16 at 21:42
  • 3
    `print(reduce(dict.get, keys, d))` – Padraic Cunningham Apr 12 '16 at 21:43
  • 1
    @PadraicCunningham: Yes, thanks. I prefer that to `dict.__getitem__` – zondo Apr 12 '16 at 21:45
  • That has the effect of returning `None` if the final key isn't found, I think, which may or may not be what you want. (`__getitem__` will throw `KeyError`.) Either would work depending on what you want. – Alex Van Liew Apr 12 '16 at 21:46
  • For key not found: one could build a partial function from `dict.get` that returns the same dict if the key is not found :) – Norman Apr 12 '16 at 21:47
  • @AlexVanLiew: That's true, but if it returns `None`, it will still throw an error. Using `__getitem__` seems hackish to me because it depends on the internal workings. – zondo Apr 12 '16 at 21:47
  • 1
    @zondo: It will only still throw an error if it wasn't the last key lookup that returns `None`. If you perform the final lookup (say, `[11, 21, 33]`) and it's not there it will return `None`, but if you have `[11, 23, 32]` it would raise a potentially confusing exception about not being able to index `NoneType`; I just find it inconsistent. Directly using magic methods isn't dependent on internal workings; I'm pretty sure they're defined in the spec and are required to be valid on any object that implements indexing (after all, `d[x]` is basically syntactic sugar for `dict.__getitem__(d, x)`). – Alex Van Liew Apr 12 '16 at 21:52
  • @AlexVanLiew: I have sort of gone your way. See my edit. – zondo Apr 12 '16 at 21:59
  • This is great, thanks! Just using `get` never occurred to me – Bahrom Apr 12 '16 at 21:59
  • @zondo: Oh yeah, of course there's `operator.getitem`, I completely forgot about that! I usually only use the `*getter`s out of that module so I forgot that was an option. – Alex Van Liew Apr 12 '16 at 22:03
0

Here's a solution I came up with that also gives back useful information when given an invalid lookup path, and allows you to dig through arbitrary json, including nested list and dict structures. (Sorry it's not a one-liner).

def get_furthest(s, path):
    '''
    Gets the furthest value along a given key path in a subscriptable structure.

    subscriptable, list -> any
    :param s: the subscriptable structure to examine
    :param path: the lookup path to follow
    :return: a tuple of the value at the furthest valid key, and whether the full path is valid
    '''

    def step_key(acc, key):
        s = acc[0]
        if isinstance(s, str):
            return (s, False)
        try:
            return (s[key], acc[1])
        except LookupError:
            return (s, False)

    return reduce(step_key, path, (s, True))
Grant Palmer
  • 208
  • 2
  • 7
-2
d = {
    11: {
        21: {
            31: 'a from dict'
        },
    },
}

l = [None] * 50
l[11] = [None] * 50
l[11][21] = [None] * 50
l[11][21][31] = 'a from list'

from functools import reduce

goodkeys = [11, 21, 31]
badkeys = [11, 12, 13]

print("Reducing dictionary (good):", reduce(lambda c,k: c.__getitem__(k), goodkeys, d))
try:
    print("Reducing dictionary (bad):", reduce(lambda c,k: c.__getitem__(k), badkeys, d))
except Exception as ex:
    print(type(ex), ex)

print("Reducing list (good):", reduce(lambda c,k: c.__getitem__(k), goodkeys, l))

try:
    print("Reducing list (bad):", reduce(lambda c,k: c.__getitem__(k), badkeys, l))
except Exception as ex:
    print(type(ex), ex)
aghast
  • 14,785
  • 3
  • 24
  • 56