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

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

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

К концу этого руководства вы научитесь:

  • Какzip() работает как в Python 3, так и в Python 2

  • Как использовать функцию Pythonzip() дляparallel iteration

  • Какcreate dictionaries на лету с помощьюzip()

Free Bonus:5 Thoughts On Python Mastery, бесплатный курс для разработчиков Python, который показывает вам план действий и образ мышления, который вам понадобится, чтобы вывести свои навыки Python на новый уровень.

Понимание функции Pythonzip()

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

>>>

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

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

Согласноofficial documentation функция Pythonzip() ведет себя следующим образом:

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

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

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

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

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

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

>>>

>>> numbers = [1, 2, 3]
>>> letters = ['a', 'b', 'c']
>>> zipped = zip(numbers, letters)
>>> zipped  # Holds an iterator object

>>> type(zipped)

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

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

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

>>>

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

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

Не передавая аргументов

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

>>>

>>> zipped = zip()
>>> zipped

>>> list(zipped)
[]

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

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

>>>

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

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

Проходя один аргумент

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

>>>

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

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

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

>>>

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

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

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

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

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

>>>

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

Поскольку5 - это длина первого (и самого короткого) объектаrange(),zip() выводит список из пяти кортежей. Есть еще 95 несогласованных элементов из второго объектаrange(). Все они игнорируются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(), чтобы получить пять кортежей с элементами изletters,numbers иlongest. Итерация останавливается только тогда, когдаlongest исчерпан. Отсутствующие элементы изnumbers иletters заполнены вопросительным знаком?, что вы указали с помощьюfillvalue.

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

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

>>>

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

>>> zipped = zip()  # Create an empty list
>>> zipped
[]

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

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

>>>

>>> # Python 3
>>> zipped = zip(range(3), 'ABCD')
>>> zipped  # Hold an iterator

>>> type(zipped)

>>> list(zipped)
[(0, 'A'), (1, 'B'), (2, 'C')]
>>> zipped = zip()  # Create an empty iterator
>>> zipped

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

Здесь ваш вызовzip() возвращает итератор. Первая итерация усекается доC, а вторая приводит к исключениюStopIteration. В 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

>>> 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 statement здесь просто заполнитель.)

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

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

Цикл над несколькими итерациями

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

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

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

>>>

>>> 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(),for иtuple unpacking, вы можете получить полезную идиому и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() с тремя итерациями для создания и возврата итератора, который генерирует кортежи из трех элементов. Это позволяет итерировать все три итерации за один раз. Нет никаких ограничений на количество итераций, которые вы можете использовать с функцией Pythonzip().

Note: Если вы хотите глубже погрузиться в циклы Pythonfor, посмотритеPython “for” Loops (Definite Iteration).

Параллельные словари

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

>>>

>>> 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() генерирует кортежи с элементами из обоих словарей. Затем вы можете распаковать каждый кортеж и получить доступ к элементам обоих словарей одновременно.

Note: Если вы хотите глубже погрузиться в итерацию словаря, посмотритеHow to Iterate Through a Dictionary in Python.

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

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

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

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

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

>>>

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

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

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

Sorting - обычная операция в программировании. Предположим, вы хотите объединить два списка и отсортировать их одновременно. Для этого вы можете использовать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().

С помощьюsorted() вы также пишете более общий фрагмент кода. Это позволит вам сортировать любые последовательности, а не только списки.

Расчет в парах

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

Element/Month январь февраль марш

Тотальная распродажа

52 000,00

51 000.00

48 000,00

Стоимость производства

46 800,00

45 900,00

43 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. Функция Pythonzip() объединяет правильные пары данных для выполнения вычислений. Вы можете обобщить эту логику для выполнения любых сложных вычислений с парами, возвращаемымиzip().

Строительные словари

dictionaries в 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) возвращает итератор, который генерирует кортежи из двух элементов. Если вы вызоветеdict() на этом итераторе, вы создадите нужный словарь. Элементыfields становятся ключами словаря, а элементыvalues представляют значения в словаре.

Вы также можете обновить существующий словарь, объединив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() обновляет словарь кортежем "ключ-значение", созданным с помощью функции Pythonzip(). С помощью этой техники вы можете легко перезаписать значениеjob.

Заключение

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

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

  • Use the zip() function как в Python 3, так и в Python 2

  • Loop over multiple iterables и параллельно выполнять разные действия над своими элементами

  • Create and update dictionaries на лету, объединяя два входных элемента

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