Самый чистый способ перебрать пару итераций разной длины, оборачивая более короткие итерируемые?
Если у меня есть две итерации разной длины, как я могу наиболее точно спарить их, повторно используя значения из более короткого, пока все значения из более длинного не будут использованы?
Например, даны два списка
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')]
Если вы не уверены, какой из них самый короткий, next
it.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
есть функция.