pythonfor-looprecursionpython-itertoolsnested-for-loop

Generating Cartesian Product of Tuple-List Pairs


I need help generating combinations from a list of tuples, where each tuple contains a string at index 0 and a list of values at index 1. For instance, consider the following setup:

params = ['sn', 'tp', 'v1', 'temp', 'slew']

list_tuple = [('Serial Number', [12345]),
              ('Test Points', ['TestpointA', 'TestpointC']),
              ('Voltage_1', [3.0, 3.3, 3.6, 0.0]),
              ('Temperature Setpoint', [0, 60]),
              ('Slew_1', [200, 400, 800, 1600, 3200, 6400])]

I've been using nested loops to generate all possible combinations of these parameters, as shown:

def generate_combinations(test_tuple, params):
    for sn in test_tuple[0][1]:
        for tp in test_tuple[1][1]:
            for v in test_tuple[2][1]:
                for temp in test_tuple[3][1]:
                    for slew in test_tuple[4][1]:
                        print(f'{params[0]}: ', sn)
                        print(f'{params[1]}: ', tp)
                        print(f'{params[2]}: ', v)
                        print(f'{params[3]}: ', temp)
                        print(f'{params[4]}: ', slew)
                        print('\n')

generate_combinations(list_tuple, params)

This approach works but isn't scalable as the depth of the nested loops corresponds to the length of the params list, which can vary. The length of each list within the tuples can also change. I need a more dynamic solution, preferably without explicitly nested loops.

I attempted to use recursion but it didn't achieve the desired results:

def not_working(list_tuple):
    for i in list_tuple:
        if isinstance(i, tuple):
            print(i[0])
            not_working(i[1])
        else:
            print(i)
            print('\n')

not_working(list_tuple)

How can I achieve the desired output dynamically, regardless of the number of tuples in list_tuple or the number of elements in each sublist, without using explicitly nested loops?


Solution

  • You can use itertools.product:

    import itertools
    data = [('Serial Number', [12345]), ('Test Points', ['TestpointA', 'TestpointC']), ('Voltage_1', [3.0, 3.3, 3.6, 0.0]), ('Temperature Setpoint', [0, 60]), ('Slew_1', [200, 400, 800, 1600, 3200, 6400])]
    params = ['sn', 'tp', 'v1', 'temp', 'slew']
    for i in itertools.product(*[b for _, b in data]):
      print('\n'.join(f'{a}:{b}' for a, b in zip(params, i)))
      print('-'*20)
    

    Output (first three results):

    sn:12345
    tp:TestpointA
    v1:3.0
    temp:0
    slew:200
    --------------------
    sn:12345
    tp:TestpointA
    v1:3.0
    temp:0
    slew:400
    --------------------
    sn:12345
    tp:TestpointA
    v1:3.0
    temp:0
    slew:800
    --------------------
    ...
    

    While itertools.product is (perhaps) the cleanest solution to this problem, a simple recursive function with a generator can be used.

    What is the difference between d and data? In the recursive function, d is mutated at every iteration by list slicing (d[i+1:]). Since the length of d decreases, and len(d) finds the length of the object d in declared in the scope of the function, it will not be finding the length of the list storing the original data, but the length of the current value passed to combination, which is decreasing at every call.

    def combination(d, current = []):
       if len(current) == len(data):
         yield current
       else:
         for i, a in enumerate(d):
           for c in a: 
             yield from combination(d[i+1:], current = current+[c])
           
    
    for i in combination([b for _, b in data]):
      print('\n'.join(f'{a}:{b}' for a, b in zip(params, i)))
      print('-'*20)
    

    Output (first three results):

    sn:12345
    tp:TestpointA
    v1:3.0
    temp:0
    slew:200
    --------------------
    sn:12345
    tp:TestpointA
    v1:3.0
    temp:0
    slew:400
    --------------------
    sn:12345
    tp:TestpointA
    v1:3.0
    temp:0
    slew:800
    --------------------