-1

If I have two iterables of different lengths, how can I most cleanly pair them, re-using values from the shorter one until all values from the longer are consumed?

For example, given two lists

l1 = ['a', 'b', 'c']
l2 = ['x', 'y']

It would be desirable to have a function fn() resulting in pairs:

>>> fn(l1, l2)
[('a', 'x'), ('b', 'y'), ('c', 'x')]

I found I could write a function to perform this as such

def fn(l1, l2):
    if len(l1) > len(l2):
        return [(v, l2[i % len(l2)]) for i, v in enumerate(l1)]
    return [(l1[i % len(l1)], v) for i, v in enumerate(l2)]

>>> fn(l1, l2)
[('a', 'x'), ('b', 'y'), ('c', 'x')]
>>> l2 = ['x', 'y', 'z', 'w']
>>> fn(l1,l2)
[('a', 'x'), ('b', 'y'), ('c', 'z'), ('a', 'w')]

However, I'm greedy and was curious what other methods exist? so that I may select the most obvious and elegant and be wary of others.

itertools.zip_longest as suggested in many similar questions is very close to my desired use case as it has a fillvalue argument which will pad the longer pairs. However, this only takes a single value, instead of wrapping back to the first value in the shorter list.

As a note: in my use case one list will always be much shorter than the other and this may allow a short-cut, but a generic solution would be exciting too!

martineau
  • 119,623
  • 25
  • 170
  • 301
ti7
  • 16,375
  • 6
  • 40
  • 68
  • just added `[*zip(A*(len(B)//len(A) + 1), B*(len(A)//len(B) + 1))]` to [how-to-zip-two-differently-sized-lists](https://stackoverflow.com/questions/19686533/how-to-zip-two-differently-sized-lists) – f5r5e5d Dec 29 '17 at 21:44

2 Answers2

1

You may use itertools.cycle() with zip to get the desired behavior.

As the itertools.cycle() document says, it:

Make an iterator returning elements from the iterable and saving a copy of each. When the iterable is exhausted, return elements from the saved copy.

For example:

>>> l1 = ['a', 'b', 'c']
>>> l2 = ['x', 'y']

>>> from itertools import cycle
>>> zip(l1, cycle(l2))
[('a', 'x'), ('b', 'y'), ('c', 'x')]

Since in your case, length of l1 and l2 could vary, your generic fn() should be like:

from itertools import cycle

def fn(l1, l2):
    return zip(l1, cycle(l2)) if len(l1) > len(l2) else zip(cycle(l1), l2)

Sample Run:

>>> l1 = ['a', 'b', 'c']
>>> l2 = ['x', 'y']

# when second parameter is shorter 
>>> fn(l1, l2)
[('a', 'x'), ('b', 'y'), ('c', 'x')]

# when first parameter is shorter
>>> fn(l2, l1)
[('x', 'a'), ('y', 'b'), ('x', 'c')]
Moinuddin Quadri
  • 46,825
  • 13
  • 96
  • 126
-1

If you're not sure which one is the shortest, next it.cycle the longest len of the two lists:

def fn(l1, l2):
    return (next(zip(itertools.cycle(l1), itertoools.cycle(l2))) for _ in range(max((len(l1), len(l2)))))

>>> list(fn(l1, l2))

[('a', 'x'), ('a', 'x'), ('a', 'x')]

itertools.cycle will repeat the list infinitely. Then, zip the two infinite lists together to get the cycle that you want, but repeated infinitely. So now, we need to trim it to the right size. max((len(l1), len(l2))) will find the longest length of the two lists, then next the infinite iterable until you get to the right length. Note that this returns a generator, so to get the output you want use list to eat the function.

rassar
  • 5,412
  • 3
  • 25
  • 41