Home > Enterprise >  How to revert a flat dictionary with exploded lists into a nested dictionary in Python
How to revert a flat dictionary with exploded lists into a nested dictionary in Python

Time:11-01

I have written a function that takes a nested structure, and flattens it while also exploding any list elements.

def __flatten_dict_explode_list(d,flat_d,is_list=False, path=[]):
    if not is_list:
        for k, v in d.items():
            if (isinstance(v, dict) and v != {}):
                __flatten_dict_explode_list(d=v,flat_d=flat_d, path=path   [k])
            elif (isinstance(v, list) and len(v) > 0 and isinstance(v[0],dict)):
                __flatten_dict_explode_list(d=v,flat_d=flat_d,is_list=True, path=path   [k])
            else:
                if '/'.join(path) '/' k not in flat_d:
                    flat_d['/'.join(path) '/' k] = v
    else:
        for j in range(0,len(d)):
            if (isinstance(d[j], dict)):
                __flatten_dict_explode_list(d=d[j],flat_d=flat_d, path=path   ['[' str(j) ']'])
            else:
                flat_d['/'.join(path) '/' '[' str(j) ']'] = d[j]
            
def flatten_dict_explode_list(d):
    flat_d = dict()
    __flatten_dict_explode_list(d,flat_d)
    return flat_d

So example, with given input

d = {'A':
         {
           'B': 1, 'C' : [{'D':1,'E':2,'F' : {'G' : 4}},{'D':2,'E':3,'F' : {'G' : 5}},{'H': [{'J':2}]}]
          }
     }

It would output

d_flat = {'A/B' : 1,
          'A/C/[0]/D' : 1,
          'A/C/[0]/E' : 2,
          'A/C/[0]/F/G' : 4,
          'A/C/[1]/D' : 2,
          'A/C/[1]/E' : 3,
          'A/C/[1]/F/G' : 5,
          'A/C/[2]/H/[0]/J' : 2
         }

But, how can I write a function that reverse this operation? Taking the flat dictionary and output a nested dictionary where also the list elements are "reverse exploded"?

def unflatten_dict(flat_d):
    ....
    return nested_dict

Appreciate any help or tips.

EDIT

I have issues with understanding how to handle the wanted list elements. Also, I am not sure if my current approach for creating the nested dictionary is best practice.

My current attempt, which does not work with list elements is.

def __unflatten_dict_help(key,val,dictionary,separator, path=[]):
    parameter_split = key.split(separator)
    parameter_split = [x for x in parameter_split if x != ""]
    for path in parameter_split[:-1]:
        if path not in dictionary:
            dictionary[path] = {}
        if type(dictionary[path]) == dict:
            dictionary = dictionary[path]
    if len(parameter_split) == 1 and parameter_split[-1] not in dictionary:
        dictionary[parameter_split[-1]] = {}
    else:
        dictionary[parameter_split[-1]] = val

def unflatten_dict(d,separator):
    output_d = dict()
    for key,val in d.items():
        __unflatten_dict_help(key,val,output_d,separator)
    return output_d

Which outputs when unflatten_dict(flat_d,"/")

{'A': 
      {'B': 1, 'C': 
                    {'[0]': {'D': 1, 'E': 2, 'F': {'G': 4}},
                     '[1]': {'D': 2, 'E': 3, 'F': {'G': 5}},
                     '[2]': {'H': 
                               {'[0]': {'J': 2}
                               }
                             }
                     }
       }
 }

CodePudding user response:

Something like this would work as __unflatten_dict_help:

def __unflatten_dict_help(key, val, d, sep):
    parts = [p if len(p) < 2 or p[0] != '[' or p[-1] != ']' else
             int(p[1:-1]) for p in key.split(separator)]
    for part, next_part in zip(parts, parts[1:]   [-1]):
        if isinstance(part, int):
            continue
        if isinstance(next_part, int):
            if next_part < 0:
                d[part] = val
                break
            if part not in d:
                d[part] = []
            if next_part == len(d[part]):
                d[part].append({})
            d = d[part][next_part]
        else:
            if part not in d:
                d[part] = {}
            d = d[part]

When running:

d = {
    'A': {
        'B': 1,
        'C': [
            {
                'D': 1,
                'E': 2,
                'F': {
                    'G': 4
                }
            },
            {
                'D': 2,
                'E': 3,
                'F': {
                    'G': 5
                }
            },
            {
                'H': [
                    {
                        'J': 2
                    }
                ]
            }
        ]
    }
}
fd = flatten_dict_explode_list(d)

print(fd)

rd = unflatten_dict(fd, '/')
print(rd)
print(rd == d)

The output is:

{'A/B': 1, 'A/C/[0]/D': 1, 'A/C/[0]/E': 2, 'A/C/[0]/F/G': 4, 'A/C/[1]/D': 2, 'A/C/[1]/E': 3, 'A/C/[1]/F/G': 5, 'A/C/[2]/H/[0]/J': 2}
{'A': {'B': 1, 'C': [{'D': 1, 'E': 2, 'F': {'G': 4}}, {'D': 2, 'E': 3, 'F': {'G': 5}}, {'H': [{'J': 2}]}]}}
True

Note that:

  • the correct way to check for type is not type(d) == dict, but isinstance(d, dict)
  • you could make the dunder helper function internal to the unflatten_dict() function, depending on whether you mind that it's accessible to others
  • this code assumes that the 'flattened' dictionary was created by your code, and that list indices don't skip or appear out of order
  • it also assumes a list will only every contain dictionaries, and none of your dictionary values at the leaf nodes are lists
  • Related