Функция zip в Python

В Python есть встроенная функция zip, которая позволяет как бы "застегивать" вместе несколько передаваемых ей итерируемых объектов (например, списков). Делает она это путем объединения элементов разных объектов, стоящих в одинаковых позициях.

>>> a = [1, 2, 3, 4]
>>> b = ['a', 'b', 'c']
>>> c = ['I', 'II', 'III', 'IV']
>>> for i in zip(a, b, c):
...     print(i)
... 
(1, 'a', 'I')
(2, 'b', 'II')
(3, 'c', 'III')

Как видно из примера объект, который возвращает функция zip, состоит из кортежей. Первый кортеж объединяет первые элементы переданных в zip списков. Второй – вторые и так далее. Количество кортежей равно длине наименьшего списка.

На самом деле объект типа zip не состоит из кортежей, он их генерирует, так как является объектом-итератором. Это значит, что получить значения из экземпляра zip можно только один раз.

>>> z = zip(a, b)
>>> z
<zip object at 0x7fc7697a4800>
>>> for i in z:
...     print(i)
... 
(1, 'a')
(2, 'b')
(3, 'c')
>>> for i in z:
...     print(i)
... 
>>> 

При этом из того, что возвращает zip, можно сразу получить структуру данных, воспользовавшись соответствующей встроенной функцией:

>>> a = [5, 6, 3]
>>> b = [1.1, -0.05, 0.3]
>>> ab = list(zip(a, b))
>>> ab
[(5, 1.1), (6, -0.05), (3, 0.3)]
>>> ab = tuple(zip(a, b))
>>> ab
((5, 1.1), (6, -0.05), (3, 0.3))

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

>>> m = [('a', 'b', 'c'), ('A', 'B', 'C')]
>>> m2 = list(zip(*m))
>>> m2
[('a', 'A'), ('b', 'B'), ('c', 'C')]
>>> 
>>> for row in m:
...     print(row)
... 
('a', 'b', 'c')
('A', 'B', 'C')
>>> 
>>> for row in m2:
...     print(row)
... 
('a', 'A')
('b', 'B')
('c', 'C')

В примере выражение *m, которое передается в функцию zip, обозначает распаковку списка. В результате в функцию передается не сам список, а два кортежа, в каждом из которых по три элемента. Первые элементы формируют первый кортеж нового объекта, вторые – второй, третьи – третий кортеж.

Если zip-экземпляр присваивать нескольким переменным, то он сразу проитерируется, и каждая переменная получит свой кортеж.

>>> a = [1, 2, 3]
>>> b = [-1, -2, -3]
>>> x, y, z = zip(a, b)
>>> x
(1, -1)
>>> y
(2, -2)
>>> z
(3, -3)

Понятно, что при этом количество переменных слева от знака присваивания должно совпадать с количеством кортежей в zip-объекте.

Часто кортежи сразу распаковывают в заголовке цикла for. Так делают, если над элементами разных итерируемых объектов надо выполнять однотипные действия или производить операции между ними самими.

>>> qty = [2, 5, 1, 4]
>>> price = [100, 91, 34, 15]
>>> for q, p in zip(qty, price):
...     print(q * p)
... 
200
455
34
60
>>> a = []
>>> b = []
>>> for i, j in zip(range(10,20), range(1,10)):
...     a.append(i)
...     b.append(j)
...
>>> a
[10, 11, 12, 13, 14, 15, 16, 17, 18]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Функция zip возвращает итератор, который останавливается, когда исчерпывается самая короткая последовательность. Когда требуется объединить итерируемые объекты разной длины так, чтобы элементы самого длинного не были утеряны, можно воспользоваться функцией zip_longest из модуля itertools.

>>> from itertools import zip_longest
>>> 
>>> a = [1, 2, 3, 4]
>>> b = ['a', 'b', 'c']
>>> 
>>> list(zip_longest(a, b))
[(1, 'a'), (2, 'b'), (3, 'c'), (4, None)]
>>> 
>>> list(zip_longest(a, b, fillvalue='?'))
[(1, 'a'), (2, 'b'), (3, 'c'), (4, '?')]

Как мы видим, если элемента не хватает, то по-умолчанию подставляется объект None. Однако с помощью параметра fillvalue можно указать свой вариант заполнения.

Практическая работа

Вычислите суммы столбцов матрицы. Используйте в программе функцию zip.