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 и есть тот край, о который цикл должен остановиться.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Чем
continueотличается отbreak? - Почему среднее считается делением на
count, а не на длину ряда? - Что сделает
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. - Переставьте пары во втором ряду местами, оставив длину прежней, и посмотрите на вывод: числа те же, пары другие, ошибки нет.
Куда это встанет в проекте
Сводка перестаёт зависеть от того, сколько лет в ряду. Один и тот же проход считает среднее, находит максимум и откладывает пропуски — хоть за пять лет, хоть за тридцать.
Долги. Проход пока написан внутри программы одним куском: чтобы посчитать то же самое по второму ряду, его придётся скопировать. Имя этому куску даст следующий урок — функция.
Ответы
Показать ответы
На вопросы
continueбросает текущий круг и переходит к следующему элементу, цикл продолжается.breakвыходит из цикла совсем, остальные элементы не проверяются.- Потому что в ряду есть год без числа. Делить на длину ряда значило бы считать пропуск нулём — то самое, о чём был прошлый урок.
zipостановится на коротком ряде и молча отбросит хвост длинного — ни ошибки, ни предупреждения. Если ряды пришли из разных источников, отчёт выйдет по неполным данным; поэтому длину либо проверяют, либо берутstrict=True.
К разминке
23.0.continueбросает текущий круг и идёт к следующему значению, поэтомуNoneне попадает в сумму, а8.0 + 15.0— попадают.
23.0
range(2021, 2026). Правая граница не входит, поэтому последний год пишут на единицу больше, чем нужен, — это первое место, где новичок теряет год.
for year in range(2021, 2026):
print(year, end=" ")
2021 2022 2023 2024 2025
range(1, len(years))начинает со второго элемента: у списка счёт с нуля. Когда нужен сам элемент, а не его номер, по списку идут напрямую:
years = [2021, 2022, 2023]
for year in years:
print(year)
2021
2022
2023
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.