Самый чистый способ перебрать пару итераций разной длины, оборачивая более короткие итерируемые?

Если у меня есть две итерации разной длины, как я могу наиболее точно спарить их, повторно используя значения из более короткого, пока все значения из более длинного не будут использованы?

Например, даны два списка

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

Хотелось бы иметь функцию fn() в результате чего в парах:

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

Я нашел, что мог бы написать функцию для выполнения этого как такового

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')]

Тем не менее, я жадный и было любопытно, какие другие методы существуют? так что я могу выбрать наиболее очевидный и элегантный и остерегаться других.

itertools.zip_longest как предложено во многих подобных вопросах, очень близко к моему желаемому варианту использования, так как он имеет fillvalue аргумент, который будет дополнять более длинные пары. Однако для этого требуется только одно значение, вместо перехода к первому значению в более коротком списке.

Как примечание: в моем случае использования один список всегда будет намного короче другого, и это может позволить сократить путь, но общее решение также будет захватывающим!

2 ответа

Решение

Вы можете использовать itertools.cycle() с zip чтобы получить желаемое поведение.

Как itertools.cycle() Документ гласит:

Сделайте итератор, возвращающий элементы из итерируемого и сохраняющий копию каждого. Когда итерация исчерпана, вернуть элементы из сохраненной копии.

Например:

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

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

Так как в вашем случае длина l1 а также l2 может варьироваться, ваш общий fn() должно быть как:

from itertools import cycle

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

Пробный прогон:

>>> 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')]

Если вы не уверены, какой из них самый короткий, nextit.cycle самый длинный len из двух списков:

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 повторю список бесконечно. Затем, zip два бесконечных списка вместе, чтобы получить цикл, который вы хотите, но повторяется бесконечно. Итак, теперь нам нужно обрезать его до нужного размера. max((len(l1), len(l2))) найдет самую длинную из двух списков, затем next бесконечное повторение, пока вы не доберетесь до нужной длины. Обратите внимание, что это возвращает генератор, поэтому, чтобы получить результат, который вы хотите использовать list есть функция.

Другие вопросы по тегам