0

This might be difficult to explain (which is part of the reason I haven't figured out how to google it properly) but I'll do my best.

I'm using MQTT and I subscribe to a topic (say "generic_topic/#")

If any publish is made to this, I update a dictionary in memory (and in a file).

So, if I get an update at "general_topic/born/to/fly" with a value of 100. I want to check if the dictionary (say gen_dict) has that value stored at that location and if not, update it.

topics = message.topic.split('/') # topic = ['general_topic', 'born', 'to', 'fly']
try:
    if (gen_dict[topics[1]][topics[2]][topics[3]] != 100):
        gen_dict[topics[1]][topics[2]][topics[3]] = 100
except:
    gen_dict[topics[1]][topics[2]][topics[3]] = 100

Problem is that there could be any number of elements in the topics list so I'm not sure how to do this recursively (or if I even need to do it recursively). Also, if there was no 'born' key in the dictionary, is there a clean way to go through the topic_list to accomplish what is being done in the "except" section of the code (because obviously that will fail since those keys do not exist)

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Scott
  • 61
  • 2
  • 9
  • So as you are going through the topics, if any are not in the dict, you want to add them at that level of nesting. Then the last one you want to set to some number? – Mark Mar 31 '23 at 22:06
  • Conceptually, I am right there with you and that is why I thought it was going to be straight forward. But when I started putting it to code, I didn't know how to do it. It's really easy if I know the "topics" list will always be a particular length. – Scott Mar 31 '23 at 22:13
  • I'm not sure that answered my question. So you have a `gen_dict = {}` then you get `['general_topic', 'born', 'to', 'fly']` you then want `gen_dict` to be turned into something like `{'general_topic': {'born': {'to': {'fly': 100}}}}`? – Mark Mar 31 '23 at 22:40
  • My mistake, I somehow missed the question. gen_dict is a dictionary and it could have a bunch of other data in it before I get an update from the broker (it very likely will). You are correct in what you assumed but it would add it to gen_dict if it doesn't already exist in the dictionary or update the value if it is there but has a different value. For example the dictionary could be the following before getting the update from the broker. { 'other': 'blah', 'general_topic': { 'born': { 'to': { 'fly': 30}}}, 'another': 324} – Scott Mar 31 '23 at 23:09
  • The dictionary could also be this: gen_dict = {'some': {'thing': 23, 'one': 393}} Technically, I'm actually not using the 'general_topic' as part of the dictionary so if it was ONLY that topic that came in and I updated an empty gen_dict ... it would be: gen_dict = {'born': {'to': {'fly': 100}}} – Scott Mar 31 '23 at 23:13
  • Beside the point, but [a bare `except` is bad practice](/q/54948548/4518341). Instead, use the specific exception you're expecting like `except KeyError`, or at least `except Exception`. – wjandrea Mar 31 '23 at 23:40
  • @wjandrea ... yes, I know ... this isn't my actual code, it was just a quick example. There are several things here I wouldn't do but they didn't relate to the question so I wasn't worried about it. – Scott Mar 31 '23 at 23:46
  • Related: [Update value of a nested dictionary of varying depth](/q/3232943/4518341). If you search for "nested dict", you'll probably find some other useful stuff. – wjandrea Mar 31 '23 at 23:53

1 Answers1

0

"I want to check if the dictionary (say gen_dict) has that value stored at that location and if not, update it."

But why? If it's the same value, updating won't change it so it'll effectively remain unchanged....


Suggested Solution 1

You can loop through the keys in topics [except for the last one] and check if they're paired to a dictionary with .get and isinstance.

# topics = ['general_topic', 'born', 'to', 'fly']
set_val = 100

cur_dict = gen_dict ## gen_dict MUST BE a dictionary
for k in topics[:-1]:
    if isinstance(cur_dict.get(k), dict): cur_dict = cur_dict[k]
cur_dict[topics[-1]] = set_val 
# if topics[-1] in cur_dict: cur_dict[topics[-1]] = set_val ## only set if path exists
# if cur_dict[topics[-1]]!=set_val: cur_dict[topics[-1]] = set_val # redundant condition

This is quite forgiving, so even if some keys are missing, it will still work

  • {'other': 'blah', 'general_topic': {'born': {'to': {'fly': 30}}}, 'another': 324} (from your comment) will become
    {'other': 'blah', 'general_topic': {'born': {'to': {'fly': 100}}}, 'another': 324}
    
  • {'born': {'fly': 30}} will become {'born': {'fly': 100}}, but
  • {'born': {'to': 10}} will become {'born': {'to': 10, 'fly': 100}}, and
  • an empty dictionary ({}) will become {'fly': 100}.

[The last 2 ({'born': {'to': 10}}, {}) will remain unchanged if you use the if topics[-1] in cur_dict condition.]


Suggested Solution 2

If you want to create the path if any part is missing [with the first key being optional], then use .setdefault as below.

set_val = 100

cur_dict = gen_dict ## gen_dict MUST BE a dictionary
for i, k in enumerate(topics[:-1]):
    if not i and not isinstance(cur_dict.get(k), dict): continue
    kval = cur_dict.setdefault(k, {})

    if not isinstance(kval, dict): cur_dict[f'prev_{k}'], cur_dict[k] = kval, {}
    cur_dict = cur_dict[k]
cur_dict[topics[-1]] = set_val 

[If the first key is not optional, then omit the if...continue line at the beginning of the loop; and using enumerate should no longer be necessary, so you can go back to for k in topics[:-1].]

  • {'other': 'blah', 'general_topic': {'born': {'to': {'fly': 30}}}, 'another': 324} will still become
    {'other': 'blah', 'general_topic': {'born': {'to': {'fly': 100}}}, 'another': 324}
    
    [just like before], but
  • {'born': {'fly': 30}} will become {'born': {'fly': 30, 'to': {'fly': 100}}}, and
  • {'born': {'to': 10}} will become {'born': {'to': {'fly': 100}, 'prev_to': 10}}, and
  • an empty dictionary ({}) will become {'born': {'to': {'fly': 100}}}.
Driftr95
  • 4,572
  • 2
  • 9
  • 21