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 — как и любой итератор:
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 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 обрезает результат по кратнейшему списку: лишний хвост пропадает молча, без ошибки и предупреждения. Кофе остался без цены, и программа об этом не сказала ни слова — ни исключения, ни даже предупреждения в выводе. Для отчётов это худший сорт бага: данные исчезли, а всё работает и печатается как ни в чём не бывало.
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-объект дважды. Итератор выдаёт пары по одной и после последней считается пустым — второй проход получает пустоту:
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 собирает первые элементы в одну колонку, вторые в другую:
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, если длины разные |
| Unzip | zip(*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)
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)) считают шумным?
Даны списки goods = ['ручка', 'тетрадь', 'пенал'] и counts = [12, 5, 3]. С помощью zip напечатайте каждую пару в виде «товар - остаток», затем строку «пар: N».
Что делает функция 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-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Списки · Урок 11
Вложенный список в Python: список списков и таблицы
Один список — лента значений, но элементом списка может быть другой список: чек из позиций, поле 3 на 3, расписание по дням. Два индекса подряд, цикл в цикле и ловушка размноженных строк.
Списки · Урок 19
Распаковка списка в Python: звёздочка и остаток
name, price = пара без индексов, swap одной строкой, *rest забирает остаток, print(*items) печатает без скобок. Одна звёздочка — четыре приёма, которые сжимают списочный код вдвое.
Списки · Урок 20
Проект на Python: инвентарь магазина на списках
Финальный проект: маленький магазин — товары, цены и остатки в трёх параллельных списках. zip собирает записи, sum и max считают агрегаты, сортировка даёт топ, comprehension находит дефицит — и всё складывается в отчёт.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
asyncio · Урок 2
Первая корутина: async def и await
async def — функция с паузами, await — «дождись результата». Звучит просто, пока не выясняется, что вызов async-функции не выполняет её тело. Разбираем оба ключевых слова на живом коде.
async функция pythonasync def что делает
Циклы · Урок 8
Вложенные циклы в Python: цикл в цикле
Цикл внутри цикла: дни и дела, план зала кинотеатра, таблица умножения и пары «каждый с каждым» — как читается вложенность и во сколько раз она умножает число шагов.
вложенные списки pythonпары элементов python
Списки · Урок 1
Список в Python: первый список и элементы в квадратных скобках
Три товара — не три переменных, а один список в квадратных скобках. Первый список, чтение элемента по индексу, замена на месте и пустой список для будущей корзины.
списки pythonсписки python для начинающих