9

Let's say I have a Python list representing ranges for some variables:

conditions = [['i', (1, 5)], ['j', (1, 2)]]

This represents that variable i ranges from 1 to 5, and inside that loop variable j ranges from 1 to 2. I want a dictionary for each possible combination:

{'i': 1, 'j': 1}
{'i': 1, 'j': 2}
{'i': 2, 'j': 1}
{'i': 2, 'j': 2}
{'i': 3, 'j': 1}
{'i': 3, 'j': 2}
{'i': 4, 'j': 1}
{'i': 4, 'j': 2}
{'i': 5, 'j': 1}
{'i': 5, 'j': 2}

The reason is that I want to iterate over them. But because the whole space is too big, I do not want to generate all of them, store them and then iterate over that list of dictionaries. I thought about using the following recursive procedure, but I need some help with the yield part. Where should it be? How do I avoid nested generators?

def iteration(conditions, currentCondition, valuedIndices):
    if currentCondition == len(conditions):
        yield valuedIndices
    else:
        cond = conditions[currentCondition]
        index = cond[0]
        lim1 = cond[1][0]
        lim2 = cond[1][1]
        for ix in range(lim1, lim2 + 1):
            valuedIndices[index] = ix
            yield iteration(conditions, currentCondition + 1, valuedIndices)

Now I would like to be able to do:

for valued_indices in iteration(conditions, 0, {}):
    ...

Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251
Noel Arteche
  • 201
  • 1
  • 3

3 Answers3

6

This is a case where it might be easier to take a step back and start fresh.

Let's start by getting the keys and the intervals separate, using a well-known trick involving zip:

>>> keys, intervals = list(zip(*conditions))
>>> keys
('i', 'j')
>>> intervals
((1, 5), (1, 2))

(The correspondence between the two preserves the original pairing; intervals[i] is the interval for the variable keys[i] for all i.)

Now, let's create proper range objects from those intervals

>>> intervals = [range(x, y+1) for x, y in intervals]
>>> list(intervals[0])
[1, 2, 3, 4, 5]
>>> list(intervals[1])
[1, 2]

We can compute the product of these range objects

>>> for v in product(*intervals):
...   print(v)
...
(1, 1)
(1, 2)
(2, 1)
(2, 2)
(3, 1)
(3, 2)
(4, 1)
(4, 2)
(5, 1)
(5, 2)

which you should recognize as the values to use for each dict. You can zip each of those values with the keys to create an appropriate set of arguments for the dict command. For example:

>>> dict(zip(keys, (1,1)))
{'i': 1, 'j': 1}

Putting this all together, we can iterate over the product to produce each dict in turn, yielding it.

def iteration(conditions):
    keys, intervals = zip(*conditions)
    intervals = [range(x, y+1) for x, y in intervals]
    yield from (dict(zip(keys, v)) for v in product(*intervals))
chepner
  • 497,756
  • 71
  • 530
  • 681
0

You can perhaps simplify a little with an internal generator comprehension and yield from it:

def dict_factory(i, j):
    r1 = range(1, i + 1)
    r2 = range(1, j + 1)
    dictgen = ({'i':x, 'j':y} for x in r1 for y in r2)
    yield from dictgen

use as:

foo = dict_factory(5, 2)
while True:
    print(next(foo))
neutrino_logic
  • 1,289
  • 1
  • 6
  • 11
  • Given conditions is passed as list of lists i suspect the OP needs a solution that would work for N number of conditions not hardcoded to 2 – Chris Doyle Mar 03 '20 at 18:15
  • @Chris Doyle good point, the function should probably be structured as ```dict_factory(*args)``` then... Hmmm... – neutrino_logic Mar 03 '20 at 18:23
0

I am not sure if you specifically need to use recursion or not but you could generate these using itertools product method to generate all the combinations. this is written in such away that you can have 1 to n conditions and it should still work and return you item by item.

from itertools import product


def iterations2(conditions):
    labels, ranges = list(zip(*conditions))
    ranges = [range(item[0], item[1] + 1) for item in ranges]
    for nums in product(*ranges):
        yield dict(zip(labels, nums))


conditions = [['i', (1, 5)], ['j', (1, 2)], ['z', (3, 6)]]
for valued_indices in iterations2(conditions):
    print(valued_indices)

OUTPUT

{'i': 1, 'j': 1, 'z': 3}
{'i': 1, 'j': 1, 'z': 4}
{'i': 1, 'j': 1, 'z': 5}
{'i': 1, 'j': 1, 'z': 6}
{'i': 1, 'j': 2, 'z': 3}
{'i': 1, 'j': 2, 'z': 4}
{'i': 1, 'j': 2, 'z': 5}
{'i': 1, 'j': 2, 'z': 6}
{'i': 2, 'j': 1, 'z': 3}
{'i': 2, 'j': 1, 'z': 4}
{'i': 2, 'j': 1, 'z': 5}
{'i': 2, 'j': 1, 'z': 6}
{'i': 2, 'j': 2, 'z': 3}
{'i': 2, 'j': 2, 'z': 4}
{'i': 2, 'j': 2, 'z': 5}
{'i': 2, 'j': 2, 'z': 6}
{'i': 3, 'j': 1, 'z': 3}
{'i': 3, 'j': 1, 'z': 4}
{'i': 3, 'j': 1, 'z': 5}
{'i': 3, 'j': 1, 'z': 6}
{'i': 3, 'j': 2, 'z': 3}
{'i': 3, 'j': 2, 'z': 4}
{'i': 3, 'j': 2, 'z': 5}
{'i': 3, 'j': 2, 'z': 6}
{'i': 4, 'j': 1, 'z': 3}
{'i': 4, 'j': 1, 'z': 4}
{'i': 4, 'j': 1, 'z': 5}
{'i': 4, 'j': 1, 'z': 6}
{'i': 4, 'j': 2, 'z': 3}
{'i': 4, 'j': 2, 'z': 4}
{'i': 4, 'j': 2, 'z': 5}
{'i': 4, 'j': 2, 'z': 6}
{'i': 5, 'j': 1, 'z': 3}
{'i': 5, 'j': 1, 'z': 4}
{'i': 5, 'j': 1, 'z': 5}
{'i': 5, 'j': 1, 'z': 6}
{'i': 5, 'j': 2, 'z': 3}
{'i': 5, 'j': 2, 'z': 4}
{'i': 5, 'j': 2, 'z': 5}
{'i': 5, 'j': 2, 'z': 6}
Chris Doyle
  • 10,703
  • 2
  • 23
  • 42