Amistad Amistad - 8 days ago 5
Python Question

Else clause in a python list comprehension with two loops

So I have a nested dict of list called dict1 as follows:

{"types": [{"name": "Chinese",
"sub":
[{
"menu": [{"name": "Ya", "cost": 1}, {"name": "Ja", "cost": 2}],
"name": "Soups"
},
{
"menu": [{"name": "Ta", "cost": 3}, {"name": "Ba", "cost": 4}],
"name": "Mains"
}]
},
{"name": "Indian(Mains)",
"menu": [{"name": "Sa", "cost": 5}, {"name": "Pa", "cost": 6}]
}
]
}


What I finally need is :

{"types": [{"name": "Chinese(Soups)",
"menu": [{"name": "Ya", "cost": 1}, {"name": "Ja", "cost": 2}]},

{"name": "Chinese(Mains)",
"menu": [{"name": "Ta", "cost": 3}, {"name": "Ba", "cost": 4}]},

{"name": "Indian(Mains)",
"menu": [{"name": "Sa", "cost": 5}, {"name": "Pa", "cost": 6}]}
]
}


So in summary, if the inner dict contains the sub key, separate it into individual dicts else leave it as it is. For reasons best known to me, I am using list comprehensions to this.This is what I have till now :

dict1['types'] = [{'name': '%s(%s)' % (outer['name'], inner['name']),
'menu': inner['menu']}
for outer in dict1['types']
if 'sub' in outer
for inner in outer['sub']]


This gives me the following output:

{"types": [{"name": "Chinese(Soups)",
"menu": [{"name": "Ya", "cost": 1}, {"name": "Ja", "cost": 2}]},

{"name": "Chinese(Mains)",
"menu": [{"name": "Ta", "cost": 3}, {"name": "Ba", "cost": 4}]}
]
}


As is evident,it gives me only 2 dicts and not 3 which is obvious since I don't have an else clause. My question is how do I use the else clause in a list comprehension with two loops in this scenario ?

EDIT
As Martijn mentioned in the comments, this is not possible with list comprehension,I am ok with other ways of accomplishing this as well by modifying the existing dict

Answer

In a list comprehension loop, if only lets you filter items you iterate over, not swap out further loops. Because if acts as a filter, there is no else as the default action in to include the iterated element.

You'd have to use an expression in your inner loop to pick one of two alternatives for the for loop to iterate over:

[{'name': '%s(%s)' % (outer['name'], inner['name']), 'menu': inner['menu']} if 'sub' in outer else inner
 for outer in dict1['types']
 for inner in (outer['sub'] if 'sub' in outer else [outer])]

So this picks either the outer['sub'] list, or the list [outer] to iterate over for the inner name. You then use a conditional expression to generate a new dictionary, or simply re-use inner (which is really outer) if no 'sub' key is present in outer.

Of course, you can simplify that by using dict.get():

[{'name': '%s(%s)' % (outer['name'], inner['name']), 'menu': inner['menu']} if 'sub' in outer else inner
 for outer in dict1['types']
 for inner in outer.get('sub', [outer])]

Demo:

>>> dict1 = {"types": [{"name": "Chinese",
...             "sub":
...                 [{
...                   "menu": [{"name": "Ya", "cost": 1}, {"name": "Ja", "cost": 2}],
...                   "name": "Soups"
...                  },
...                  {
...                   "menu": [{"name": "Ta", "cost": 3}, {"name": "Ba", "cost": 4}],
...                   "name": "Mains"
...                 }]
...            },
...            {"name": "Indian(Mains)",
...             "menu": [{"name": "Sa", "cost": 5}, {"name": "Pa", "cost": 6}]
...            }
...          ]
... }
>>> [{'name': '%s(%s)' % (outer['name'], inner['name']), 'menu': inner['menu']} if 'sub' in outer else inner
...  for outer in dict1['types']
...  for inner in outer.get('sub', [outer])]
[{'name': 'Chinese(Soups)', 'menu': [{'name': 'Ya', 'cost': 1}, {'name': 'Ja', 'cost': 2}]}, {'name': 'Chinese(Mains)', 'menu': [{'name': 'Ta', 'cost': 3}, {'name': 'Ba', 'cost': 4}]}, {'name': 'Indian(Mains)', 'menu': [{'name': 'Sa', 'cost': 5}, {'name': 'Pa', 'cost': 6}]}]
>>> from pprint import pprint
>>> pprint(_)
[{'menu': [{'cost': 1, 'name': 'Ya'}, {'cost': 2, 'name': 'Ja'}],
  'name': 'Chinese(Soups)'},
 {'menu': [{'cost': 3, 'name': 'Ta'}, {'cost': 4, 'name': 'Ba'}],
  'name': 'Chinese(Mains)'},
 {'menu': [{'cost': 5, 'name': 'Sa'}, {'cost': 6, 'name': 'Pa'}],
  'name': 'Indian(Mains)'}]

Personally, I'd use a generator function here, rather than a single list comprehension:

def flattened_menu(types):
    for entry in types:
        if 'sub' in entry:
            # flatten out nested menu entries
            for subentry in entry['sub']:
                yield {
                    'name': '{0[name]}({1[name]})'.format(entry, subentry),
                    'menu': subentry['menu']}
        else:
            yield entry

dict1['types'] = list(flattened_menu(dict1['types']))
Comments