Расказывает Рювен Лёрнер, преподаватель
Многие слышали о функции zip в Python, а кто-то даже регулярно ей пользуется. Сегодня мы (из интереса и для общего развития) опишем, как можно реализовать её самому с помощью list comprehensions.
Для начала поясню, что вообще делает функция zip, для тех, кто с ней раньше не сталкивался:
>>> s = 'abc'
>>> t = (10, 20, 30)
>>> zip(s,t)
[('a', 10), ('b', 20), ('c', 30)]
То есть функция берёт на вход несколько списков и создаёт из них список (в Python 3 создаётся не list, а специальный zip-объект) кортежей, такой, что первый элемент полученного списка содержит кортеж из первых элементов всех списков-аргументов. Таким образом, если ей передать три списка, то она отработает следующим образом:
>>> s = 'abc'
>>> t = (10, 20, 30)
>>> u = (-5, -10, -15)
>>> list(zip(s,t,u))
[('a', 10, -5), ('b', 20, -10), ('c', 30, -15)]
В общем-то, функция отработает даже для одного iterable-объекта, результатом будет последовательность из кортежей, в каждом из которых будет по одному элементу. Но это, пожалуй, не самый распространенный способ применения zip. Я часто использую zip, например, для создания словарей:
>>> names = ['Tom', 'Dick', 'Harry']
>>> ages = [50, 35, 60]
>>> dict(zip(names, ages))
{'Harry': 60, 'Dick': 35, 'Tom': 50}
Это весьма удобно, не находите? Каждый раз, когда я рассказываю о zip на своих уроках, у меня спрашивают о том, что будет, если в функцию передать массивы разной длины. Ответ простой — победит более короткий:
>>> s = 'abc'
>>> t = (10, 20, 30, 40)
>>> list(zip(s,t))
[('a', 10), ('b', 20), ('c', 30)]
Однако, если вам необходимо, чтобы для каждого из элементов более длинного массива в результирующем списке был создан кортеж из одного элемента, вы можете использовать zip_longest из пакета itertools.
Есть одна возможность в Python, которая мне нравится даже больше, чем zip. Это списковое включение (англ. list comprehension). Именно поэтому, когда один из студентов недавно спросил меня, можем ли мы реализовать zip сами с помощью списковых включений, я просто не смог устоять.
Как же нам этого добиться? Начнём с первого, что приходит на ум:
[(s[i], t[i]) # создаём кортеж из двух элементов
for i in range(len(s))] # для индексов от 0 до len(s) - 1
В общем-то всё! Это работает. Но есть несколько моментов, которые всё же стоит доработать в этом методе.
Во-первых, оригинальная функция могла работать с массивами разной длины. Поэтому вместо range(len(s))
нам стоит использовать range(len(x))
, где x
— наиболее короткая последовательность. Для этого достаточно поместить все последовательности в один список, отсортировать этот список по длине элементов и выяснить длину элемента, оказавшегося под нулевым индексом:
>>> s = 'abcd'
>>> t = (10, 20, 30)
>>> sorted((s,t), key=len)
[(10, 20, 30), 'abcd']
Совмещаем это с предыдущим кодом:
>>> s = 'abcd'
>>> t = (10, 20, 30)
>>> sorted((s,t), key=len)
[(10, 20, 30), 'abcd']
Это ещё не все доработки, а выражение уже получается слишком длинным. Пожалуй, выяснение наименьшей длины стоит вынести в отдельную функцию (заодно сделаем так, чтобы она вычисляла наикратчайшую последовательность из неограниченного количества аргументов):
>>> def shortest_sequence_range(*args):
return range(len(sorted(args, key=len)[0]))
>>> [(s[i], t[i])
for i in shortest_sequence_range(s,t) ]
Что осталось теперь? Как уже говорилось выше, Python 3 создаёт не список, а специальный zip-объект, возвращая итератор от него. Это сделано для того, чтобы код не ломался при обработке исключительно длинных последовательностей. Это можно реализовать, но уже не с помощью спискового включения (которое всегда возвращает список), а с помощью генератора. К счастью, для этого достаточно поменять квадратные скобки на круглые:
>>> def shortest_sequence_range(*args):
return range(len(sorted(args, key=len)[0]))
>>> g = ((s[i], t[i])
for i in shortest_sequence_range(s,t) )
>>> for item in g:
print(item)
('a', 10)
('b', 20)
('c', 30)
Готово! Мы реализовали свой полностью рабочий zip. Вы можете потренироваться и самостоятельно подумать, как ещё можно улучшить этот алгоритм.
Перевод публикации «Implementing “zip” with list comprehensions»