ПрограммированиеBackend разработчик

Как работает встроенная функция zip() в Python, для чего она применяется и какие тонкие места есть при обработке последовательностей разной длины?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса

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

Проблема

Нередко нужно обработать несколько списков (или другого рода последовательности) одновременно — например, пройтись по паре ключ-значение или обработать координаты точка-пара. Самостоятельная синхронизация индексов — источник ошибок и нечитабельности кода, особенно для коллекций разной длины.

Решение

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

Пример кода:

names = ['Alice', 'Bob', 'Charlie'] ages = [24, 27, 30] for name, age in zip(names, ages): print(f'{name} is {age} years old')

Можно развернуть zip с помощью *:

pairs = [(1, 'a'), (2, 'b'), (3, 'c')] nums, chars = zip(*pairs) print(nums) # (1, 2, 3) print(chars) # ('a', 'b', 'c')

Ключевые особенности:

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

Вопросы с подвохом.

Что будет, если передать zip() коллекции разной длины?

zip() остановится, когда достигнет конца самой короткой коллекции — остальные элементы длинных коллекций игнорируются.

print(list(zip([1,2,3], ['a','b']))) # [(1, 'a'), (2, 'b')]

Как получить кортежи, дополняя более короткие последовательности значением по умолчанию?

Стандартная zip() так не умеет, но есть itertools.zip_longest для такого поведения:

from itertools import zip_longest for a, b in zip_longest([1,2], ['x','y','z'], fillvalue=None): print(a, b) # 1 x # 2 y # None z

Можно ли "распаковать" результат zip(), чтобы снова получить исходные списки?

Да, если все исходные коллекции были одной длины и результат не был изменён, оператор * позволяет развернуть zip.

pairs = [(1,2), (3,4)] a, b = zip(*pairs) print(a) # (1, 3) print(b) # (2, 4)

Типовые ошибки и анти-паттерны

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

Пример из жизни

Негативный кейс

Обработка связанных коллекций разной длины, не учитывая особенности zip:

lst1 = [1,2,3,4] lst2 = ['a','b'] for x, y in zip(lst1, lst2): print(x, y) # 1 a # 2 b # (3,4) и 'c', 'd' из lst1 не обработались

Плюсы:

  • Просто и понятно, если последовательности гарантированно одинаковой длины.

Минусы:

  • Потеря значений, если фактическая длина коллекций различается.

Позитивный кейс

Использование zip_longest с fillvalue, чтобы не потерять ни один элемент:

from itertools import zip_longest lst1 = [1,2,3,4] lst2 = ['a','b'] for x, y in zip_longest(lst1, lst2, fillvalue='?'): print(x, y) # 1 a # 2 b # 3 ? # 4 ?

Плюсы:

  • Гарантирована обработка всех элементов.
  • Можно явно задать "пустое" значение.

Минусы:

  • Нужно импортировать внешний модуль.
  • Важно не забыть про fillvalue, иначе — None по умолчанию.