Используем zip() для парной итерации

Сайт webdevblog.ru опубликовал перевод статьи «Using the Python zip() Function for Parallel Iteration». Представляем его вашему вниманию.

Застежка типа "молния". По-английски это zipper, отсюда zip.

В этой статье:

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

К концу этого урока вы узнаете:

  • Как работает zip() в Python 3 и Python 2
  • Каким образом использовать функцию zip() для парной итерации
  • Как создавать словари на лету с помощью zip()

Функции zip()

zip() доступен во встроенном пространстве имен (built-in namespace). Если вы используете dir() для проверки __builtins__, тогда увидите zip() в конце формируемого списка:

>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', ..., 'zip']

Вы можете видеть, что ‘zip‘ является последней записью в списке доступных объектов.

Согласно официальной документации, функция Python zip() работает следующим образом:

Возвращает итератор кортежей, где i-й кортеж содержит i-й элемент из каждой последовательности аргументов или итераций. Итератор останавливается, когда самая короткая входная итерация исчерпана. С единственным итерируемым аргументом, он возвращает итератор из 1 кортежа. Без аргументов возвращает пустой итератор. (Источник)

Если вы не сразу все поняли то вы поймете это определение в оставшейся части учебника. Работая с примерами кода, вы увидите, что операции с zip работают так же, как обычная молния на сумке или паре джинсов. Блокирующие пары зубцов по обеим сторонам молнии стягиваются вместе, чтобы закрыть отверстие. Фактически, эта визуальная аналогия идеально подходит для понимания zip(), так как функция была названа в честь физических застежек-молний!

Использование zip() в Python

Функция zip() в Python определяется как zip(* iterables). Функция принимает итераторы (то есть последовательности) в качестве аргументов и возвращает то же итератор. Этот итератор генерирует серию кортежей, содержащих элементы из каждой итерации. zip() может принимать любые типы итераций, такие как файлы, списки, кортежи, словари, наборы и т. д.

Передача n аргументов

Если вы используете zip() с n аргументами, то функция вернет итератор, который генерирует кортежи длины n. Чтобы увидеть это в действии, взгляните на следующий блок кода:

>>> numbers = [1, 2, 3]
>>> letters = ['a', 'b', 'c']

>>> zipped = zip(numbers, letters)
>>> zipped  # Holds an iterator object
<zip object at 0x7fa4831153c8>
>>> type(zipped)
<class 'zip'>

>>> list(zipped)
[(1, 'a'), (2, 'b'), (3, 'c')]

Здесь используется zip(numbers, letters) для создания итератора, который создает кортежи в форме (x, y). В этом случае значения x берутся из numbers, а значения y берутся из letters. Обратите внимание, как функция Python zip() возвращает итератор. Чтобы получить окончательный объект списка, вам нужно использовать list() для использования итератора.

Если вы работаете с последовательностями, такими как списки, кортежи или строки, то ваши итерации гарантированно будут оцениваться слева направо. Это означает, что результирующий список кортежей будет иметь вид [(numbers[0], letters[0]), (numbers[1], letters[1]),…, (numbers[n], letters[n])]. Однако для других типов итераций (например, наборов) вы можете увидеть странные результаты:

>>> s1 = {2, 3, 1}
>>> s2 = {'b', 'a', 'c'}
>>> list(zip(s1, s2))
[(1, 'a'), (2, 'c'), (3, 'b')]

В этом примере s1 и s2 являются установленными объектами, которые не хранят свои элементы в каком-либо определенном порядке. Это означает, что кортежи, возвращаемые функцией zip(), будут иметь элементы, которые спарены случайным образом. Если вы собираетесь использовать функцию Python zip() с неупорядоченными итерациями, такими как наборы, то об этом следует помнить.

Использование без аргументов

Вы также можете вызвать zip() без аргументов. В этом случае вы просто получите пустой итератор:

>>> zipped = zip()
>>> zipped
<zip object at 0x7f196294a488>
>>> list(zipped)
[]

Здесь вызывается zip() без аргументов, поэтому в переменной zipped содержится пустой итератор. Если вы используете итератор с list(), то вы также увидите пустой список.

Вы также можете попытаться заставить пустой итератор напрямую выдавать элемент. В этом случае вы получите исключение StopIteration:

>>> zipped = zip()
>>> next(zipped)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Когда вызывается next() для zipped, Python пытается получить следующий элемент. Однако, поскольку zipped содержит пустой итератор, и вытащить нечего, то Python вызывает исключение StopIteration.

Передача одного аргумента

Функция Python zip() также может принимать только один аргумент. Результатом будет итератор, который возвращает серию кортежей из 1 элемента:

>>> a = [1, 2, 3]
>>> zipped = zip(a)
>>> list(zipped)
[(1,), (2,), (3,)]

Это может быть не так полезно, но все равно работает. Возможно, вы сможете найти несколько примеров использования этого поведения zip() самостоятельно!

Как видите, вы можете вызывать функцию zip() с таким количеством входных итераций, сколько вам нужно. Длина получаемых кортежей всегда будет равна числу итераций, которые вы передаете в качестве аргументов. Вот пример с тремя итерациями:

>>> integers = [1, 2, 3]
>>> letters = ['a', 'b', 'c']
>>> floats = [4.0, 5.0, 6.0]
>>> zipped = zip(integers, letters, floats)  # Three input iterables
>>> list(zipped)
[(1, 'a', 4.0), (2, 'b', 5.0), (3, 'c', 6.0)]

Здесь вы вызываете функцию zip() с тремя итерациями, поэтому каждый из получаемых кортежей имеет три элемента.

Передавая аргументы неравной длины

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

В этих случаях количество элементов, которые выдает zip(), будет равно длине самой короткой итерации. Остальные элементы в любых более длинных итерациях будут полностью игнорироваться zip(), как вы можете видеть здесь:

>>> list(zip(range(5), range(100)))
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)]

Поскольку 5 — это длина первого (и самого короткого) объекта range()zip() выводит список из пяти кортежей. Из второго объекта range() все еще есть 95 непревзойденных элементов. Все они игнорируются zip(), так как больше нет элементов из первого объекта range() для завершения пар.

Если для вас важны конечные или несопоставленные значения, вы можете использовать itertools.zip_longest() вместо zip(). С помощью этой функции пропущенные значения будут заменены тем, что вы передадите аргументу fillvalue (по умолчанию None). Итерации будут продолжаться до тех пор, пока не будет исчерпана самая длинная итерация:

>>> from itertools import zip_longest
>>> numbers = [1, 2, 3]
>>> letters = ['a', 'b', 'c']
>>> longest = range(5)
>>> zipped = zip_longest(numbers, letters, longest, fillvalue='?')
>>> list(zipped)
[(1, 'a', 0), (2, 'b', 1), (3, 'c', 2), ('?', '?', 3), ('?', '?', 4)]

Здесь используется itertools.zip_longest(), чтобы получить пять кортежей с элементами из numbersletters и longest. Итерация останавливается только тогда, когда самый длинный список longest будет израсходован. Недостающие элементы из numbers и letters заполнены знаком вопроса ?, который был указали с помощью fillvalue.

Сравнение zip() в Python 3 и 2

Функция zip() в Python работает по-разному в обеих версиях языка. В Python 2 zip() возвращает список кортежей. Результирующий список усекается до длины самого короткого итерируемого списка. Если вы вызываете zip() без аргументов, вы получите пустой list в ответ:

>>> # Python 2
>>> zipped = zip(range(3), 'ABCD')
>>> zipped  # Hold a list object
[(0, 'A'), (1, 'B'), (2, 'C')]
>>> type(zipped)
<type 'list'>
>>> zipped = zip()  # Create an empty list
>>> zipped
[]

В этом случае вызов функции Python zip() возвращает список кортежей. Когда вы вызываете zip() без аргументов, вы получаете пустой список.

В Python 3, zip() возвращает итератор. Этот объект возвращает кортежи по требованию и может быть пройден только один раз. Итерация заканчивается исключением StopIteration после исчерпания самой короткой входной итерации. Если вы не укажете аргументы для zip(), то функция вернет пустой итератор:

>>> # Python 3
>>> zipped = zip(range(3), 'ABCD')
>>> zipped  # Hold an iterator
<zip object at 0x7f456ccacbc8>

>>> type(zipped)
<class 'zip'>

>>> list(zipped)
[(0, 'A'), (1, 'B'), (2, 'C')]

>>> zipped = zip()  # Create an empty iterator
>>> zipped
<zip object at 0x7f456cc93ac8>

>>> next(zipped)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    next(zipped)
StopIteration

Здесь вызов zip() возвращает итератор. В Python 3 вы также можете эмулировать поведение zip() из Python 2, заключая возвращенный итератор в вызов list(). В таком случае вернется список кортежей.

Если вы регулярно используете Python 2, обратите внимание, что использование zip() с длинными входными данными может непреднамеренно потребить много памяти. В этих ситуациях рассмотрите возможность использования вместо него itertools.izip(*iterables). Эта функция создает итератор, который объединяет элементы из каждого из итераций. Он производит тот же эффект, что и zip() в Python 3:

>>> # Python 2
>>> from itertools import izip
>>> zipped = izip(range(3), 'ABCD')
>>> zipped
<itertools.izip object at 0x7f3614b3fdd0>
>>> list(zipped)
[(0, 'A'), (1, 'B'), (2, 'C')]

В этом примере вызывается itertools.izip() для создания итератора. Когда вы используете возвращенный итератор с помощью list(), вы получаете список кортежей, как если бы вы использовали zip() в Python 3. Итерация останавливается, когда самая короткая входная итерация будет исчерпана.

Если вам действительно нужно написать код, который ведет себя одинаково как в Python 2, так и в Python 3, то вы можете использовать прием, подобный следующему:

try:
    from itertools import izip as zip
except ImportError:
    pass

Здесь, если izip() доступен из itertools, вы будете знать, что находитесь в Python 2, и izip() будет импортирован с использованием псевдонима zip. В противном случае ваша программа вызовет ImportError, и вы узнаете, что находитесь на Python 3. (Здесь выражение pass является просто заполнителем.)

С помощью этого трюка вы можете безопасно использовать одну и туже функцию zip() во всем коде. При запуске ваша программа автоматически выберет и использует правильную версию.

Итак, мы рассмотрели, как работает функция zip() в Python, и узнали о некоторых из ее наиболее важных функций. Теперь пришло время засучить рукава и рассмотреть реальные примеры!

Обработка нескольких списков

Циклическая обработка нескольких списков является одним из наиболее распространенных случаев использования функции zip() в Python. Если вам нужно пройтись по нескольким спискам, кортежам или любой другой последовательности, то вполне вероятно, что вы воспользуетесь zip(). Этот раздел покажет вам, как использовать zip() для итерации нескольких списков одновременно.

Параллельное прохождение списков

Функция zip() позволяет выполнять параллельные итерации по двум и более последовательностям. Поскольку zip() генерирует кортежи, вы можете распаковать их в заголовке цикла for:

>>> letters = ['a', 'b', 'c']
>>> numbers = [0, 1, 2]
>>> for l, n in zip(letters, numbers):
...     print(f'Letter: {l}')
...     print(f'Number: {n}')
...
Letter: a
Number: 0
Letter: b
Number: 1
Letter: c
Number: 2

Здесь вы перебираете серию кортежей, возвращаемых zip(), и распаковываете элементы в l и n. Когда вы комбинируете zip() для циклов и распаковки кортежей, вы можете получить полезную и Pythonic идиому для прохождения двух и более итераций одновременно.

Вы также можете выполнить более двух итераций в одном цикле for. Рассмотрим следующий пример с тремя входными итерациями:

>>> letters = ['a', 'b', 'c']
>>> numbers = [0, 1, 2]
>>> operators = ['*', '/', '+']
>>> for l, n, o in zip(letters, numbers, operators):
...     print(f'Letter: {l}')
...     print(f'Number: {n}')
...     print(f'Operator: {o}')
...
Letter: a
Number: 0
Operator: *
Letter: b
Number: 1
Operator: /
Letter: c
Number: 2
Operator: +

В этом примере использовано zip() с тремя списками для создания и возврата итератора, который генерирует кортежи из 3 элементов. Это позволяет итерировать все три итерации за один раз. Нет ограничений на количество итераций, которые можно использовать с функцией zip().

Параллельная обработка словарей

В Python 3.6 и более поздних версиях словари являются упорядоченными коллекциями, то есть они хранят свои элементы в том же порядке, в котором они были созданы. Если вы воспользуетесь этой возможностью, то можете использовать функцию zip() для безопасного и последовательного перебора нескольких словарей:

>>> dict_one = {'name': 'John', 'last_name': 'Doe', 'job': 'Python Consultant'}
>>> dict_two = {'name': 'Jane', 'last_name': 'Doe', 'job': 'Community Manager'}
>>> for (k1, v1), (k2, v2) in zip(dict_one.items(), dict_two.items()):
...     print(k1, '->', v1)
...     print(k2, '->', v2)
...
name -> John
name -> Jane
last_name -> Doe
last_name -> Doe
job -> Python Consultant
job -> Community Manager

Здесь параллельно перебирается dict_one и dict_two. В этом случае zip() генерирует кортежи с элементами из обоих словарей. Затем вы можете распаковать каждый кортеж и получить доступ к элементам обоих словарей одновременно.

Обратите внимание, что в приведенном выше примере порядок оценки слева направо гарантирован. Вы также можете использовать функцию zip() для параллельного перебора множеств. Однако вы должны учитывать, что, в отличие от словарей в Python 3.6, наборы не сохраняют свои элементы в порядке. Если вы забудете эту деталь, конечный результат вашей программы может оказаться не совсем тем, чего вы хотите или ожидаете.

Распаковка последовательности

На форумах для новых Pythonistas часто возникает вопрос: «Если есть функция zip(), то почему нет функции unzip(), которая делает обратное?»

Причина, по которой в Python нет функции unzip(), заключается в том, что противоположностью zip() является …, zip(). Вы помните, что функция zip() работает так же, как настоящая молния? Пока приведенные примеры показали, как Python закрывает молнию. Итак, как распаковывать объекты Python?

Допустим, у вас есть список кортежей и вы хотите разделить элементы каждого кортежа на независимые последовательности. Для этого вы можете использовать zip() вместе с оператором распаковки *, например так:

>>> pairs = [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]
>>> numbers, letters = zip(*pairs)
>>> numbers
(1, 2, 3, 4)
>>> letters
('a', 'b', 'c', 'd')

Здесь есть список кортежей, содержащих смешанные данные. Затем был использован оператор распаковки *, чтобы распаковать данные, создав два разных списка (numbers и letters).

Параллельная сортировка

Сортировка — обычная операция в программировании. Предположим, вы хотите объединить два списка и отсортировать их одновременно. Для этого вы можете использовать zip() вместе с .sort() следующим образом:

>>> letters = ['b', 'a', 'd', 'c']
>>> numbers = [2, 4, 3, 1]
>>> data1 = list(zip(letters, numbers))
>>> data1
[('b', 2), ('a', 4), ('d', 3), ('c', 1)]
>>> data1.sort()  # Sort by letters
>>> data1
[('a', 4), ('b', 2), ('c', 1), ('d', 3)]
>>> data2 = list(zip(numbers, letters))
>>> data2
[(2, 'b'), (4, 'a'), (3, 'd'), (1, 'c')]
>>> data2.sort()  # Sort by numbers
>>> data2
[(1, 'c'), (2, 'b'), (3, 'd'), (4, 'a')]

В этом примере сначала объединяются два списка с помощью zip() и затем сортируются. Обратите внимание, как data1 сортируются по letters, а data2 — по numbers.

Вы также можете использовать sorted() и zip() вместе для достижения аналогичного результата:

>>> letters = ['b', 'a', 'd', 'c']
>>> numbers = [2, 4, 3, 1]
>>> data = sorted(zip(letters, numbers))  # Sort by letters
>>> data
[('a', 4), ('b', 2), ('c', 1), ('d', 3)]

В этом случае sorted() проходит через итератор, сгенерированный zip (), и сортирует элементы по letters, и все за один раз. Этот подход может быть немного быстрее, так как нам понадобятся только два вызова функций: zip() и sorted().

Вычисления в парах

Вы можете использовать функцию zip() для быстрых вычислений. Предположим, у нас есть следующие данные в электронной таблице:

Element/MonthJanuaryFebruaryMarch
Total Sales52,000.0051,000.0048,000.00
Production Cost46,800.0045,900.0043,200.00

Нам нужно использовать эти данные для расчета ежемесячной прибыли. zip() может предоставить быстрый способ сделать эти вычисления:

>>> total_sales = [52000.00, 51000.00, 48000.00]
>>> prod_cost = [46800.00, 45900.00, 43200.00]
>>> for sales, costs in zip(total_sales, prod_cost):
...     profit = sales - costs
...     print(f'Total profit: {profit}')
...
Total profit: 5200.0
Total profit: 5100.0
Total profit: 4800.0

Здесь рассчитывается прибыль за каждый месяц, вычитая costs из sales. Функция zip() объединяет правильные пары данных для выполнения расчетов. Вы можете обобщить эту логику для выполнения любых сложных вычислений с парами, возвращаемыми функцией zip().

Создание словарей

Словари Python — это очень полезная структура данных. Иногда вам может потребоваться создать словарь из двух разных, но тесно связанных последовательностей. Удобный способ добиться этого — использовать dict() и zip() вместе. Например, предположим, что вы извлекли данные из формы или базы данных. Теперь у вас есть следующие списки данных:

>>> fields = ['name', 'last_name', 'age', 'job']
>>> values = ['John', 'Doe', '45', 'Python Developer']

С этими данными необходимо создать словарь для дальнейшей обработки. В этом случае можно использовать dict() вместе с zip() следующим образом:

>>> a_dict = dict(zip(fields, values))
>>> a_dict
{'name': 'John', 'last_name': 'Doe', 'age': '45', 'job': 'Python Developer'}

Здесь создается словарь, который объединяет два списка. zip(fields, values) возвращает итератор, который генерирует кортежи из 2 элементов. Если вы вызовете dict() для этого итератора, то таким образом создается нужный словарь. Элементы полей становятся ключами словаря, а элементы значений представляют значения в словаре.

Вы также можете обновить существующий словарь, комбинируя zip() с dict.update(). Предположим, что Джон меняет свою работу, и вам нужно обновить словарь. Вы можете сделать что-то вроде следующего:

>>> new_job = ['Python Consultant']
>>> field = ['job']
>>> a_dict.update(zip(field, new_job))
>>> a_dict
{'name': 'John', 'last_name': 'Doe', 'age': '45', 'job': 'Python Consultant'}

Здесь dict.update() обновляет словарь с помощью кортежа значения ключа, который вы создали с помощью функции zip(). С помощью этой техники вы можете легко перезаписать значение job.

Заключение

Из этого урока вы узнали, как использовать функцию zip() в Python. zip() может получить несколько списков в качестве входных данных. Он возвращает итератор, который может генерировать кортежи с парными элементами из каждого аргумента. Получающийся итератор может быть весьма полезен, когда вам нужно обработать несколько списков в одном цикле и одновременно выполнить некоторые действия над их элементами.

Теперь вы можете:

  • Использовать функцию zip() в Python 3 и Python 2
  • Обрабатывать одновременно несколько списков и выполнять различные действия над их элементами параллельно
  • Создавать и обновлять словари на лету, объединяя два входных списка вместе

Мы также написали несколько примеров, которые вы можете использовать в качестве отправной точки для реализации собственных решений с помощью функции zip() в Python. Не стесняйтесь изменять эти примеры, когда вы будете углубленно изучать zip()!

[python_ad_block]