Search code examples
pythonpython-itertools

Python itertools.product challenge to expand a dict with tuples


Given a dictionary like this with some items being tuples...

params = {
 'a': 'static',
 'b': (1, 2),
 'c': ('X', 'Y')
}

I need the "product" of the items into a list of dict like this, with the tuples expanded so each item in b will be matched with each item in c...

[{ 'a': 'static', 'b': 1, 'c': 'X' },
 { 'a': 'static', 'b': 1, 'c': 'Y' },
 { 'a': 'static', 'b': 2, 'c': 'X' },
 { 'a': 'static', 'b': 2, 'c': 'Y')}]

I can easily separate the initial input into a list of non-tuple items and tuple items, and apply the key of each tuple to the values as a "tag" prior to multiplication so they look like this: 'b##1', 'b##2', 'c##X', 'c##Y'. Then parse those back into the above dict after multiplication. If I would always see 2 tuple items (like b and c), I could easily pass both to itertools.products. But there could be 0..n tuple items, and product() doesn't multiply a list of lists in this way. Can anyone think of a solution?

TAG = '##'      
# separate tuples and non-tuples from the input, and prepend the key of each tuple as a tag on the value to parse out later
for key, value in params.items():
    if type(value) is tuple:
        for x in value:
            tuples.append(f'{key}{TAG}{x}')
    else:
        non_tuples.append({key: value})
print(list(product(tuples))      # BUG: doesn't distribute each value of b with each value of c

Solution

  • product takes multiple iterables, but the key thing to remember is that an iterable can contain a single item. In cases where a value in your original dict isn't a tuple (or maybe a list), you want to convert it to a tuple containing a single value and pass that to product:

    params_iterables = {}
    for k, v in params.items():
        if isinstance(v, (tuple, list)):
            params_iterables[k] = v     # v is already a tuple or a list
        else:
            params_iterables[k] = (v, ) # A tuple containing a single value, v
    

    which gives:

    params_iterables = {'a': ('static',), 'b': (1, 2), 'c': ('X', 'Y')}
    

    Then, simply get the product of the values in params_iterables:

    result = []
    for values in product(*params_iterables.values()):
        result.append(dict(zip(params, values)))
    

    The dict(zip(params, values)) line creates a dict where the first element of values is assigned the first key in params, and so on. This dict is then appended to result, which gives the desired output:

    [{'a': 'static', 'b': 1, 'c': 'X'},
     {'a': 'static', 'b': 1, 'c': 'Y'},
     {'a': 'static', 'b': 2, 'c': 'X'},
     {'a': 'static', 'b': 2, 'c': 'Y'}]