3

I have class with a nested dictionary data object. I need to get all the key values from it. What's the best efficient way to do this?

I'm stuck with following:

for k,v in data.items():
    print v.keys()

This is the data:

data = {
    "BANK": {
        "no_data": "INT",
    },
    "SHOCK": {
        "drop": "NOTI",
        "rise": "NOTI",
        "high_risk": "ALERT",
    },
    "OFFLINE": {"online": None, "offline_few": "ALERT"},
}
shx2
  • 61,779
  • 13
  • 130
  • 153
sg_sg94
  • 2,248
  • 4
  • 28
  • 56

5 Answers5

3

An elegant way to concatenate lists (your value.keys() lists) into one is using a double-loop list comprehenstion, like this:

nested_keys = [
    key
    for val in data.values()
    for key in val.keys()]
shx2
  • 61,779
  • 13
  • 130
  • 153
  • This would get *keys of the values dictionaries* only. This wouldn't get `"BANK,"SHOCK","OFFLINE"`. OP's question is vague though. – Ch3steR Apr 09 '20 at 12:03
1

Using a generator:

def all_keys(d):
    for k, v in d.items():
        yield k
        # assume that anything with `items` property will be a mapping
        # this can be replaced with: `isinstance(v, dict)` or `isinstance(v, collections.Mapping)`
        if hasattr(v, 'items'):  
            yield from all_keys(v)

On your input this produces:

data = {
    "BANK": {
        "no_data": "INT",
    },
    "SHOCK": {
        "drop": "NOTI",
        "rise": "NOTI",
        "high_risk": "ALERT",
    },
    "OFFLINE": {"online": None, "offline_few": "ALERT"},
}
print(list(all_keys(data)))
# ['BANK', 'no_data', 'SHOCK', 'drop', 'rise', 'high_risk', 'OFFLINE', 'online', 'offline_few']
norok2
  • 25,683
  • 4
  • 73
  • 99
0

Recursive Function

Gets all keys at all levels of nested dictionary

def get_keys(d, result = None): 
  # use default of None to fix issue noted by @Ch3steR
  # namely: http://effbot.org/zone/default-values.htm
  if result is None:
    result = []

  for k, v in d.items():
    if isinstance(v, dict):
        result.append(k)
        get_keys(v, result)
    else:
      result.append(k)

  return result

Test

print(get_keys(data)) 

Output

['BANK', 'no_data', 'SHOCK', 'drop', 'rise', 'high_risk', 'OFFLINE', 'online', 'offline_few']
DarrylG
  • 16,732
  • 2
  • 17
  • 23
  • You will encounter https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument – Ch3steR Apr 09 '20 at 12:13
  • You can check it yourself run the function twice. After calling it once `get_keys.__defaults__` will be `(['BANK', 'no_data', 'SHOCK', 'drop', 'rise', 'high_risk', 'OFFLINE', 'online', 'offline_few'],)` after second time `get_keys.__defaults__` will be `(['BANK', 'no_data', 'SHOCK', 'drop', 'rise', 'high_risk', 'OFFLINE', 'online', 'offline_few', 'BANK', 'no_data', 'SHOCK', 'drop', 'rise', 'high_risk', 'OFFLINE', 'online', 'offline_few'])` – Ch3steR Apr 09 '20 at 12:13
  • Here's [link](https://repl.it/repls/RelievedInfatuatedArchitect) to check. – Ch3steR Apr 09 '20 at 12:19
  • @Ch3steR, my solution will not have this issue, probably; could you check? – Piyush Singh Apr 09 '20 at 12:21
  • @PiyushSingh If you have a mutable default parameter don't mutate it in-place. Just run it twice or thrice and use `func.__defaults__` will you get to know. – Ch3steR Apr 09 '20 at 12:23
  • @PiyushSingh Checked your function it encountered the same [least astonishment and mutable default argument](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument) issue – Ch3steR Apr 09 '20 at 12:24
  • @Ch3steR--thanks for the reminder and updated post to set default param on call. – DarrylG Apr 09 '20 at 12:26
0

If all your "actual" key-value pairs are at a certain depth, for example for depth 1, you can go like:

data = {
    "BANK": {
        "no_data": "INT",
    },
    "SHOCK": {
        "drop": "NOTI",
        "rise": "NOTI",
        "high_risk": "ALERT",
    },
    "OFFLINE": {"online": None, "offline_few": "ALERT"},
}
dic = {k:v for val in data.values() for k,v in val.items()}

But if you dont know that:

data = {
    "BANK": {
        "no_data": "INT",
    },
    "SHOCK": {
        "drop": "NOTI",
        "rise": "NOTI",
        "high_risk": "ALERT",
    },
    "online": None, 
    "offline_few": "ALERT"
}

In this case you need to use recursion:

def unnest(dic, final=dict()):
    for key, val in dic.items():
        if not isinstance(val, dict):
            final[key] = val
        else:
            dic2 = dict()
            for k, v in val.items():
                dic2[k] = v
            unnest(dic2, final)
    return final    

dic = unnest(data, {}) #every use of the function should have {} to solve issue pointed by @Ch3steR

In any case, once you have the "un-nested" dictionary it is trivial to print out the keys:

print(dic.keys())
Piyush Singh
  • 2,736
  • 10
  • 26
  • Check your function it will encounter [least astonishment and mutable default argument](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument). To check if it does do `unnest.__defaults__` – Ch3steR Apr 09 '20 at 12:26
  • @Ch3steR, thanks for sharing that. I didnt know such a thing existed in python. Updated my answer to solve the issue, I just had to make sure that each time the function is called it starts with a fresh empty dictionary. – Piyush Singh Apr 09 '20 at 12:43
  • @Ch3steR, here is a piece of code that shows then solves the issue: https://repl.it/repls/UselessWorseNewsaggregator – Piyush Singh Apr 09 '20 at 12:50
0

You could use a NestedDict. First install ndicts

pip install ndicts

Then

from ndicts.ndicts import NestedDict
data = {
    "BANK": {
        "no_data": "INT",
    },
    "SHOCK": {
        "drop": "NOTI",
        "rise": "NOTI",
        "high_risk": "ALERT",
    },
    "OFFLINE": {"online": None, "offline_few": "ALERT"},
}
nd = NestedDict(data)

The result

>>> list(nd.keys())
[('BANK', 'no_data'), ('SHOCK', 'drop'), ('SHOCK', 'rise'), ('SHOCK', 'high_risk'), ('OFFLINE', 'online'), ('OFFLINE', 'offline_few')]
edd313
  • 1,109
  • 7
  • 20