Convert a Python list of dictionaries with list value to a flat list of dictionary with single value

Viewed 1226

The problem statement

Given a list of dictionaries:

p = [
     {'x': 1, 
      'a': [12, 14, 16], 
      'b': [13, 15], 
      'y': 8},
     {'x': 2, 
      'a': [22, 24], 
      'b': [23, 25, 27], 
      'y': 9}
    ]

with some keys ('a' and 'b') in the form of lists, while others ('x' and 'y') as single values-- I'd like to create a rather flat dictionary, where the list key-values are segregated into separate keys 'a_1', 'a_2', ..., 'b_1', 'b_2', ..., etc. while keeping the single key-values as it is.

t = [
     {'x': 1, 
      'a_1': 12, 
      'a_2': 14, 
      'a_3': 16, 
      'b_1': 13, 
      'b_2': 15, 
      'y': 8},
     {'x': 2, 
      'a_1': 22, 
      'a_2': 24, 
      'b_1': 23, 
      'b_2': 25, 
      'b_3': 27, 
      'y': 9}
    ]

A partial solution

I was able to convert just one key 'a' having a list value to a dictionary with single values with the code:

q = [{'a_'+str(i+1): obj['a'][i]
        for i in range(len(obj['a']))} 
            for obj in p]
print (q)

which gives an output looking like:

[
 {'a_1': 12, 
  'a_2': 14, 
  'a_3': 16},
 {'a_1': 22, 
  'a_2': 24}
]

but no idea how to do that for multiple keys... particularly to do this for those keys whose value is of type list, and simply keep the key-value otherwise... and finally merging all the key-values into one dictionary within the outer loop for obj in p.

5 Answers

you can do as follows:

def parse_dict(d):
    new_dict = {}
    for key,val in d.items():
        if isinstance(val,list):
            new_dict.update({f'{key}_{i+1}':v for i,v in enumerate(val)})
        else:
            new_dict[key] = val
    return new_dict

result = [parse_dict(d) for d in p]

the output of result:

[{'x': 1, 'a_1': 12, 'a_2': 14, 'a_3': 16, 'b_1': 13, 'b_2': 15, 'y': 8},
 {'x': 2, 'a_1': 22, 'a_2': 24, 'b_1': 23, 'b_2': 25, 'b_3': 27, 'y': 9}]

the main idea was to separate responsibilities by creating a new function and then think elementwise...

hope this helps more then just solves the issue.

update:

if you insist in not making a function and finding a one (long and unreadable) one liner:

[ {**{key:val for key,val in d.items() if not isinstance(val,list)},**{f'{key}_{i}' if isinstance(val,list)else key : v if isinstance(val,list)else val for key,val in d.items() if isinstance(val,list) for i,v in enumerate(val)}} for d in p]

output:

[{'x': 1, 'y': 8, 'a_0': 12, 'a_1': 14, 'a_2': 16, 'b_0': 13, 'b_1': 15},
 {'x': 2, 'y': 9, 'a_0': 22, 'a_1': 24, 'b_0': 23, 'b_1': 25, 'b_2': 27}]

(notice that the order has changed)

Using a nested iteration, isinstance to check the type and enumerate to get index of item

Ex:

result = []
for i in p:
    temp = {}
    for k, v in i.items():
        if isinstance(v, list):
            for idx,j in enumerate(v, 1):
                temp[f"{k}_{idx}"] = j
        else:
            temp[k] = v
    result.append(temp)
print(result)                
                

Output:

[{'a_1': 12, 'a_2': 14, 'a_3': 16, 'b_1': 13, 'b_2': 15, 'x': 1, 'y': 8},
 {'a_1': 22, 'a_2': 24, 'b_1': 23, 'b_2': 25, 'b_3': 27, 'x': 2, 'y': 9}]

You can use a list comprehension:

p = [{'x': 1, 'a': [12, 14, 16], 'b': [13, 15], 'y': 8}, {'x': 2, 'a': [22, 24], 'b': [23, 25, 27], 'y': 9}]
r = [{a if not isinstance(b, list) else f'{a}_{l}':b if not isinstance(b, list) else j 
     for a, b in i.items() for l, j in enumerate((b if isinstance(b, list) else [b]), 1)} 
         for i in p]

Output:

[{'x': 1, 'a_1': 12, 'a_2': 14, 'a_3': 16, 'b_1': 13, 'b_2': 15, 'y': 8}, {'x': 2, 'a_1': 22, 'a_2': 24, 'b_1': 23, 'b_2': 25, 'b_3': 27, 'y': 9}]

What you demanding is little tricky but I gonna try my best. Lets continue it,

So the way i faund is using functions(definitions)

def get_new(l):
    N = l.copy()
    for a in range(len(N)):
        for b in N[a].keys():
            if b in ['a','b']:
                i = 1
                while N[a][b]: # while N[a][b] is not empty.
                    key = str(b) + '_' + str(i)
                    i += 1
                    N[a][key] = N[a][b].pop(0)
                del N[a][b]
    
    return N

above function take your initial list and return the list you demanded

Function loops in dictionary and inside it key and for every 'a' and 'b' key it will change keys every list value keys like str(a)+'_'+i

i is perfectly index +1

values as N[a][b][i] # because it pop out index 0 every time.

delete N[a][b] when its particular key-value is prepared.

I was able to come up with (again a partial) list-comprehension style solution:

t = []
for obj in p:
    t.append(dict([]))
    [t[-1].update({key+'_'+str(i+1): val[i]
        for i in range(len(obj[key]))}) 
           if isinstance(obj[key], list) else
     t[-1].update({key: val})
        for key, val in zip(obj.keys(), obj.values()) 
    ]

One explicit loop for the items (obj) in p, where a new dictionary is created inside the loop, and term(s) added to it depending on whether the item-value is a list or not... thanks to my preceding answerers for pointing out the isinstance(obj, type) function.

So, printing t gives:

[{'x': 1, 'a_1': 12, 'a_2': 14, 'a_3': 16, 'b_1': 13, 'b_2': 15, 'y': 8},
 {'x': 2, 'a_1': 22, 'a_2': 24, 'b_1': 23, 'b_2': 25, 'b_3': 27, 'y': 9}]

A not-to-be-so-proud-of plus point of this solution is: it doesn't use the f'{key}_{i+1}' feature (which, though both the elegant solvers have used, seems rather a bit obscure to me-- pardon my ignorance) !

Related