Shanraq.org Shanraq.org
Циклы: for, range и то, что zip делает молча
IT

Python: от данных до своей сводки Урок 8 из 56

Циклы: for, range и то, что zip делает молча

Восьмой урок курса по Python. Одно сравнение вместо тридцати: цикл проходит ряд сам, `continue` пропускает год без данных, накопитель считает среднее — 11,52% за пять лет. Плюс два подвоха: `zip` молча обрезает ряд по короткому, а `while` без выхода уходит за край и падает.

Зачем это нужно

В прошлом уроке одно и то же сравнение было написано три раза. Пять лет — уже пятнадцать строк, тридцать лет — программа, которую невозможно ни прочитать, ни поправить.

Цикл убирает повтор целиком: правило пишется один раз, а строк в ряду может быть сколько угодно. С него начинается работа с настоящими данными — в них не три числа, а тысячи.

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

Сразу целиком

Файл cikl.py. Запуск: python cikl.py из окружения.

Программа длиннее прежних — сорок четыре строки. Обязательное в ней — первый блок: проход по ряду, continue и накопитель. Остальные три показывают zip, enumerate, break и while; их достаточно узнать в лицо и вернуться, когда понадобятся.

"""Урок 8: цикл — по ряду проходит программа, а не вы.

В прошлом уроке одно сравнение было написано три раза. Здесь оно пишется один
раз, а лет может быть хоть тридцать.
"""

# Инфляция за год, %: Казахстан и мир. Числа из первого урока, Всемирный банк.
kz = {2021: 8.0, 2022: 15.0, 2023: 14.5, 2024: 8.7, 2025: 11.4, 2026: None}
world = {2021: 3.5, 2022: 8.1, 2023: 5.8, 2024: 3.0, 2025: 3.0}

print("== весь ряд, пропуск не в счёт")
total = 0.0
count = 0
for year, value in kz.items():
    if value is None:
        print(f"{year}: данных нет")
        continue
    total += value
    count += 1
    print(f"{year}: {value:5.1f}%")
print(f"лет с числами: {count}, среднее: {total / count:.2f}%")

print()
print("== два ряда рядом")
for number, (here, there) in enumerate(zip(kz.values(), world.values()), start=1):
    print(f"{number}. Казахстан {here:5.1f} | мир {there:4.1f}")

print()
print("== первый год выше десяти процентов")
for year in range(2021, 2026):
    if kz[year] > 10:
        print(f"{year}: {kz[year]}%")
        break

print()
print("== сколько лет подряд с конца выше мировой")
year = 2025
streak = 0
# Условие проверяет и то, что год вообще есть в ряду: без этого цикл уходит
# в 2020-й и падает с KeyError. for кончается сам, while — только когда скажут.
while year in world and kz[year] > world[year]:
    streak += 1
    year -= 1
print(f"подряд: {streak}, последний такой год: {year + 1}")

Выводит:

== весь ряд, пропуск не в счёт
2021:   8.0%
2022:  15.0%
2023:  14.5%
2024:   8.7%
2025:  11.4%
2026: данных нет
лет с числами: 5, среднее: 11.52%

== два ряда рядом
1. Казахстан   8.0 | мир  3.5
2. Казахстан  15.0 | мир  8.1
3. Казахстан  14.5 | мир  5.8
4. Казахстан   8.7 | мир  3.0
5. Казахстан  11.4 | мир  3.0

== первый год выше десяти процентов
2022: 15.0%

== сколько лет подряд с конца выше мировой
подряд: 5, последний такой год: 2021

Разбор

for берёт элементы, а не считает номера

for year, value in kz.items():

В большинстве языков цикл заводит счётчик и лезет по номеру. В Python for берёт сами элементы: из списка — значения, из словаря — ключи, а из .items() — пары, которые тут же раскладываются на две переменные. Это та же распаковка, что была в уроке про кортежи.

Отсюда правило, которое экономит половину ошибок: если вам нужен элемент — берите элемент, а не номер.

Образ. Стопка чеков в руке. Вы не спрашиваете «дай мне чек номер четыре» — вы берёте следующий, пока стопка не кончится.

continue пропускает, break останавливает

if value is None:
    print(f"{year}: данных нет")
    continue

continue бросает текущий круг и переходит к следующему элементу. Ровно это нужно с пропуском из прошлого урока: год без числа печатается отдельной строкой и не попадает в среднее.

break — другое: он выходит из цикла совсем. В третьем блоке он останавливает перебор на первом же годе выше десяти процентов, и 2023–2025 не проверяются вовсе.

Накопитель заводят до цикла

total = 0.0
count = 0
for ...:
    total += value
    count += 1

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

Считаем два числа, а не одно: сумму и количество. Потому что делить надо не на длину ряда, а на количество лет с данными — пропуск не в счёт, и именно из-за этого среднее вышло 11,52, а не 9,6.

zip молча обрезает по короткому

Посмотрите на второй блок вывода: пять строк, хотя в kz шесть лет.

for number, (here, there) in enumerate(zip(kz.values(), world.values()), start=1):

zip соединяет ряды попарно и останавливается на том, который кончился раньше. У нас в kz шесть значений, в world — пять, поэтому 2026 год исчез из вывода. Ни ошибки, ни предупреждения.

Здесь это как раз кстати: у 2026 года всё равно нет числа. Но привычка опасная: если два ряда пришли из разных источников и один короче, zip тихо отрежет хвост, а отчёт выйдет по неполным данным. Когда длина важна, её проверяют явно — len(a) == len(b) — или берут zip(a, b, strict=True), который на разной длине падает вместо того, чтобы молчать.

И то, что важнее обрезки: zip соединяет по позициям, а не по годам. Здесь ключи в обоих рядах идут по порядку, и пары получились верные. Придут ряды из разных источников — порядок ключей может оказаться другим, и zip молча сопоставит 2021 год с 2022-м. Ни равная длина, ни strict=True от этого не спасают: они считают элементы, а не сверяют ключи.

Отсюда правило: два ряда сравнивают по ключу, а не по позиции. Годы для этого и есть: for year in kz: ... world[year]. zip берут там, где позиция и есть смысл, — как в нашем выводе, где рядом печатаются два числа за один и тот же год, потому что оба ряда взяты из одного места и в одном порядке.

enumerate считает круги за вас

enumerate(..., start=1)

Нужен номер строки — не заводите счётчик руками. enumerate отдаёт пару «номер, элемент», а start=1 говорит, что людям нумерацию удобнее читать с единицы, а не с нуля.

range — это не список, а рецепт

for year in range(2021, 2026):

range(2021, 2026) даёт 2021, 2022, 2023, 2024, 2025 — правый край не входит, как и в срезах из урока о списках. Это то же правило, и запоминать его второй раз не надо.

Важное: range не создаёт список в памяти. range(1_000_000) занимает столько же места, сколько range(5), — он выдаёт числа по одному. Это первая встреча с ленивостью; подробно она будет в уроке про генераторы.

while кончается только тогда, когда вы скажете

while year in world and kz[year] > world[year]:

for заканчивается сам: элементы кончились — цикл вышел. while крутится, пока условие истинно, и следить за выходом приходится вам.

Первая версия этой программы была написана без year in world — и упала:

KeyError: 2020

Все пять лет оказались выше мировой инфляции, цикл дошёл до 2021 года, шагнул в 2020-й, которого в ряду нет, и запросил несуществующий ключ. Проверка year in world и есть тот край, о который цикл должен остановиться.

Карта урока

Карта урока: проход, пропуск и край

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Чем continue отличается от break?
  2. Почему среднее считается делением на count, а не на длину ряда?
  3. Что сделает zip, если один ряд короче другого, и чем это опасно?

Разминка

Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.

1. Предскажите. Что напечатает эта программа?

total = 0
for value in [8.0, None, 15.0]:
    if value is None:
        continue
    total += value
print(total)

2. Заполните пропуск. Вместо ... поставьте то, что напечатает годы с 2021 по 2025 включительно.

for year in ...:
    print(year, end=" ")

3. Почините. Программа печатает два года из трёх. Найдите, где потерялся первый.

years = [2021, 2022, 2023]
for i in range(1, len(years)):
    print(years[i])

Задание

Обязательное. Дано:

prices = [520.0, 546.0, None, 498.0, 515.0]

Пройдите список циклом и напечатайте номер месяца (нумерация с единицы) и цену; месяц без числа напечатайте отдельной строкой и в счёт не берите. В конце напечатайте, сколько месяцев с числом и среднее по ним. Затем найдите первый месяц дороже 530 и напечатайте его номер — дальше не ищите.

Ожидаемый вывод:

1: 520.0
2: 546.0
3: данных нет
4: 498.0
5: 515.0
месяцев с числом: 4, среднее: 519.75
первый дороже 530: месяц 2

Готово, когда: вывод совпадает построчно; пропуск не попал ни в сумму, ни в счётчик; поиск первого месяца останавливается сам, а не проходит список до конца.

На своих данных. Возьмите свой ряд из пяти-семи чисел — цены одного товара по месяцам, свои расходы, что угодно измеримое, — и поставьте в одном месте None. Пройдите по нему циклом: пропуск напечатайте отдельной строкой и не считайте, для остальных выведите значение и посчитайте сумму, количество и среднее.

По желанию.

  • Найдите break-ом первый месяц, где число превысило среднее.
  • Сложите два своих ряда через zip и специально сделайте их разной длины — посмотрите, сколько строк напечатается.
  • Замените zip(a, b) на zip(a, b, strict=True) и прочитайте, что скажет Python.
  • Переставьте пары во втором ряду местами, оставив длину прежней, и посмотрите на вывод: числа те же, пары другие, ошибки нет.

Куда это встанет в проекте

Сводка перестаёт зависеть от того, сколько лет в ряду. Один и тот же проход считает среднее, находит максимум и откладывает пропуски — хоть за пять лет, хоть за тридцать.

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

Ответы

Показать ответы

На вопросы

  1. continue бросает текущий круг и переходит к следующему элементу, цикл продолжается. break выходит из цикла совсем, остальные элементы не проверяются.
  2. Потому что в ряду есть год без числа. Делить на длину ряда значило бы считать пропуск нулём — то самое, о чём был прошлый урок.
  3. zip остановится на коротком ряде и молча отбросит хвост длинного — ни ошибки, ни предупреждения. Если ряды пришли из разных источников, отчёт выйдет по неполным данным; поэтому длину либо проверяют, либо берут strict=True.

К разминке

  1. 23.0. continue бросает текущий круг и идёт к следующему значению, поэтому None не попадает в сумму, а 8.0 + 15.0 — попадают.
23.0
  1. range(2021, 2026). Правая граница не входит, поэтому последний год пишут на единицу больше, чем нужен, — это первое место, где новичок теряет год.
for year in range(2021, 2026):
    print(year, end=" ")
2021 2022 2023 2024 2025
  1. range(1, len(years)) начинает со второго элемента: у списка счёт с нуля. Когда нужен сам элемент, а не его номер, по списку идут напрямую:
years = [2021, 2022, 2023]
for year in years:
    print(year)
2021
2022
2023

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.