Перейти к содержанию

432 уроков, 26 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 14 из 20 Начальный 15 мин 100 XP

Zip в Python: параллельные списки в пары

Товары лежат в одном списке, цены — в другом, а идти по ним нужно синхронно. Функция zip собирает пары одной строкой: цикл без индексов, обрезание по кратнейшему и unzip через звёздочку.

Редакция Питоники

Параллельные списки — самая частая форма данных в небольших программах: товары в одном списке, цены в другом, и i-й товар отвечает i-й цене. Чтобы пройтись по обоим сразу, без знания zip остаётся один инструмент — индексный цикл. Он работает, но шумный: range, len, двойные скобки. Функция zip делает то же самое одной строкой и заодно открывает приёмы, которые понадобятся в проекте раздела: строгий режим для разных длин и распаковку пар обратно в списки.

Старый способ: цикл по индексам

Пока zip не знаком, пары собирают через range(len): счётчик бегает по обоим спискам сразу, и на каждом шаге из каждого списка достаётся элемент по индексу:

пары через индексный цикл
names = ['молоко', 'хлеб', 'сыр']
prices = [89, 45, 520]

for i in range(len(names)):
    print(names[i], '-', prices[i])
Вывод
молоко - 89
хлеб - 45
сыр - 520

Вывод правильный, но код громоздкий: range, len и две пары квадратных скобок на одну строку цикла. Перепутать местами имена и цены в скобках легко, а ошибка не словится — оба списка существуют, просто печать кривая. И сущностей в игре три: два данных и счётчик, который сам по себе никому не нужен.

zip собирает пары

Функция zip принимает несколько списков и на каждом шаге отдаёт кортеж из элементов с одним номером: первые элементы — первая пара, вторые — вторая. Чтобы увидеть пары, результат оборачивают в list — как и любой итератор:

zip склеивает два списка
names = ['молоко', 'хлеб', 'сыр']
prices = [89, 45, 520]

pairs = zip(names, prices)
print(list(pairs))
print(type(pairs).__name__)
Вывод
[('молоко', 89), ('хлеб', 45), ('сыр', 520)]
zip

Пары — кортежи: они перебираются как элементы списка, а внутри распаковываются в две переменные. Именно это делает цикл по параллельным спискам читаемым. А вот прямой печати zip-объект не выносит — покажем ловушку рядом:

так печатать не стоит
print(zip(['молоко', 'хлеб'], [89, 45]))
zip печатается служебной строкой вида <zip object at 0x7f8a2c3d4e50>: адрес памяти у объекта свой на каждый запуск, вывод не сверишь. Пары показывает list(zip(...)) или цикл for по zip.
цикл по парам без индексов
names = ['молоко', 'хлеб', 'сыр']
prices = [89, 45, 520]

for name, price in zip(names, prices):
    print(name, '-', price, 'руб')
Вывод
молоко - 89 руб
хлеб - 45 руб
сыр - 520 руб

Заголовок for name, price in zip(names, prices) распаковывает каждый кортеж в две переменные — тот же приём, что подробно разбирается в уроке про распаковку. Вывод в точности совпал с индексной версией, но исчезли range, len и скобки: код читается как вопрос, на который отвечает цикл.

Ловушка: zip обрезает по кратнейшему

Параллельные списки живут недолго в идеальном состоянии: для нового товара цену ещё не занесли, и длины разошлись. Посмотрим, что сделает zip с такими списками:

списки разной длины
names = ['молоко', 'хлеб', 'сыр', 'кофе']
prices = [89, 45, 520]

print(len(names), len(prices))
print(list(zip(names, prices)))
Вывод
4 3
[('молоко', 89), ('хлеб', 45), ('сыр', 520)]

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

strict=True превращает потерю в ошибку
names = ['молоко', 'хлеб', 'сыр', 'кофе']
prices = [89, 45, 520]

try:
    list(zip(names, prices, strict=True))
except ValueError as e:
    print('Ошибка:', e)
Вывод
Ошибка: zip() argument 2 is shorter than argument 1

Сообщение называет виновника прямо: второй аргумент короче первого. В рабочей программе такая проверка стоит в месте склейки данных — ошибка падает там, где списки собрались, а не через три экрана в отчёте. Каркас try-except для её ловли разобран в курсе про исключения.

zip одноразовый: итератор истощается

Второй сюрприз подстерегает того, кто решит использовать zip-объект дважды. Итератор выдаёт пары по одной и после последней считается пустым — второй проход получает пустоту:

второй проход по тому же zip
pairs = zip(['а', 'б', 'в'], [1, 2, 3])

print(len(list(pairs)))
print(len(list(pairs)))
Вывод
3
0

Первая list вычерпала все три пары, второй достался пустой итератор — и ноль вместо данных. Правило простое: либо сразу превращайте zip в список — pairs = list(zip(names, prices)) — и используйте его сколько нужно, либо обходите zip-объект ровно один раз в цикле. Написать pairs = zip(...) в одном месте и прочитать его в двух — самая частая ошибка темы.

Распаковка: zip(*pairs) разворачивает пары

Обратная операция называется unzip: из списка пар получить обратно две колонки. Делается она той же функцией zip со звёздочкой — звёздочка подаёт пары как отдельные аргументы, и zip собирает первые элементы в одну колонку, вторые в другую:

unzip через звёздочку
pairs = [('молоко', 89), ('хлеб', 45), ('сыр', 520)]

names, prices = zip(*pairs)
print(names)
print(prices)
Вывод
('молоко', 'хлеб', 'сыр')
(89, 45, 520)

На входе zip(*pairs) получил три кортежа как три отдельных аргумента, на выходе отдал два кортежа-колонки, и распаковка разложила их в names и prices. Никакой новой функции — та же zip, просто аргументы поданы звёздочкой; вся механика разбирается в уроке про распаковку и звёздочку. Колонки вышли кортежами: если дальше нужен список, оберните в list.

ПриёмЗаписьЧто происходит
Парыzip(names, prices)Кортежи (names[i], prices[i]) по одному номеру
Цикл без индексовfor name, price in zip(...)Каждая пара распаковывается в две переменные
Строгий режимzip(a, b, strict=True)ValueError, если длины разные
Unzipzip(*pairs)Пары обратно собираются в колонки

Пары из zip — первый уровень вложенности: полный список списков и таблицы разбираются в уроке про вложенные списки. А в итоговом проекте zip свяжет три параллельных списка магазина — товары, цены и остатки — и превратит их в отчёт с агрегатами и топом товаров.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

a = [10, 20, 30]
b = [1, 2]
print(len(list(zip(a, b))))
names = ['а', 'б']
prices = [5, 7, 9]
total = 0
for name, price in zip(names, prices):
    total += price
print(total, len(list(zip(names, prices))))
pairs = [(1, 'а'), (2, 'б')]
nums, letters = zip(*pairs)
print(nums)
print(letters)
Проверь себя
0 / 7

1. Что вернёт list(zip(['а', 'б'], [1, 2, 3]))?

2. Сколько пар даст zip из списков длиной 5 и 2?

3. Как пройтись по двум параллельным спискам без индексов?

4. Почему второй вызов list(pairs) по тому же zip-объекту возвращает пустой список?

5. Что делает zip(*pairs)?

6. Что произойдёт в zip(a, b, strict=True), если список a длиннее b?

7. Почему старый способ for i in range(len(names)) считают шумным?

Карточки терминов
Запомнено: 0 / 7
Практика

Даны списки goods = ['ручка', 'тетрадь', 'пенал'] и counts = [12, 5, 3]. С помощью zip напечатайте каждую пару в виде «товар - остаток», затем строку «пар: N».

practice.py
Вопросы и ответы по уроку

Что делает функция zip в Python?

Принимает несколько списков и возвращает итератор кортежей: на каждом шаге пара из элементов с одним номером. list(zip(['а', 'б'], [1, 2])) даст [('а', 1), ('б', 2)]. Типичное применение — параллельные списки в цикле for без индексов: for name, price in zip(names, prices).

Урок 19: распаковка и звёздочка

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

Обычный zip обрежет результат по кратнейшему списку, и хвост длинного потеряется без предупреждения. Если рассинхрон недопустим, включите strict=True (Python 3.10+): тогда несовпадение длин бросит ValueError. Без strict сверяйте len до цикла.

Как распаковать zip обратно в два списка?

Той же функцией со звёздочкой: names, prices = zip(*pairs). Звёздочка подаёт пары как отдельные аргументы, zip собирает первые элементы пар в одну колонку, вторые — во вторую. Колонки получаются кортежами; список делается через list().

Как идти по спискам разной длины без потери данных?

Функцией zip_longest из стандартного модуля itertools: она дополняет короткий список значением-заполнителем (по умолчанию None) до длины длинного. Для задач, где лишний хвост не нужен, остаётся обычный zip со strict-проверкой.

Понравился урок? Сошлитесь на него

«zip обрезает результат по кратнейшему списку: лишний хвост пропадает молча, без ошибки и предупреждения.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.