Python: от данных до своей сводки Урок 9 из 56
Функции: имя для расчёта и assert, который ловит неверное число
Девятый урок курса по Python. Проход по ряду получает имя и считает любой ряд: Казахстан — 11,52%, мир — 4,68%. Плюс `assert`, который останавливает расчёт на пустом ряде, `lambda` в `sorted(key=)` и измеренная ловушка: `into=[]` в заголовке копит значения между вызовами.
Зачем это нужно
В прошлом уроке проход по ряду был написан прямо в программе. Чтобы посчитать то же самое по второму ряду, его надо скопировать — и с этого мгновения у вас две копии, которые расходятся при первой же правке.
Функция даёт куску расчёта имя. После этого он вызывается сколько угодно раз, живёт в одном месте и правится один раз.
Второе, что появляется сегодня, для курса о данных важнее синтаксиса: проверка внутри расчёта. Неверное число не кричит — оно спокойно доходит до отчёта и выглядит как настоящее. assert — первый способ поставить на его пути преграду.
Сразу целиком
Файл func.py. Запуск: python func.py из окружения.
Обязательное — две функции и их вызовы: def, return, аргумент со значением по умолчанию и assert. Всё, что после, — lambda, *args, область видимости — разобрано ниже отдельно и в задании не нужно.
"""Урок 9: функция — имя для куска расчёта.
В прошлом уроке проход по ряду был написан внутри программы. Чтобы посчитать
то же самое по второму ряду, его пришлось бы скопировать. Имя решает это.
"""
# Инфляция за год, %: Казахстан и мир. Числа из первого урока, Всемирный банк.
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}
def average(series):
"""Среднее по ряду; годы без числа не в счёт."""
total = 0.0
count = 0
for value in series.values():
if value is None:
continue
total += value
count += 1
assert count > 0, "в ряду нет ни одного числа"
return total / count
def above(series, other, since=2021):
"""Годы, в которых первый ряд выше второго, начиная с since.
Год, в котором числа нет хоть в одном ряду, не в счёт.
"""
years = []
for year, value in series.items():
if year < since or value is None or other.get(year) is None:
continue
if value > other[year]:
years.append(year)
return years
print("== среднее за пять лет")
print(f"Казахстан: {average(kz):.2f}%")
print(f"мир: {average(world):.2f}%")
print()
print("== годы выше мировой")
print("все: ", above(kz, world))
print("с 2023:", above(kz, world, since=2023))
print()
print("== три самых высоких года")
pairs = []
for year, value in kz.items():
if value is not None:
pairs.append((year, value))
top = sorted(pairs, key=lambda pair: pair[1], reverse=True)
print(top[:3])
Выводит:
== среднее за пять лет
Казахстан: 11.52%
мир: 4.68%
== годы выше мировой
все: [2021, 2022, 2023, 2024, 2025]
с 2023: [2023, 2024, 2025]
== три самых высоких года
[(2022, 15.0), (2023, 14.5), (2025, 11.4)]
Разбор
def, отступ и return
def average(series):
...
return total / count
def заводит имя, скобки перечисляют аргументы, двоеточие и отступ задают тело — как у if и for. return отдаёт значение наружу и на этом заканчивает работу функции: строки после него не выполняются.
Функция без return тоже работает, но возвращает None — то самое «числа нет» из седьмого урока. Забытый return даёт не ошибку, а пустоту в расчёте, и ищут её потом долго.
Строка в тройных кавычках сразу после def — docstring, описание функции. Мы уже видели его в начале файлов; здесь он объясняет не программу, а один кусок, и это то место, где стоит написать, что функция делает с пропусками.
Сказать — мало, надо и сделать. В above пропуск проверяется с обеих сторон: other.get(year) is None закрывает и «года нет в чужом ряду», и «год есть, а числа нет». Проверь мы только свой ряд — value > other[year] однажды встретил бы None и упал бы с TypeError вместо ответа. Функция, которая обещает работать с пропусками, обязана знать про все пропуски, которые до неё дойдут.
Образ. Рецепт с названием. Пока он в голове, вы рассказываете его каждый раз заново; записанный и названный, он передаётся одной фразой.
Аргумент со значением по умолчанию и вызов по имени
def above(series, other, since=2021):
since=2021 — значение по умолчанию: вызвали без него, получили с ним. above(kz, world, since=2023) передаёт аргумент по имени, и в вызове видно, что значит 2023.
Это стоит привычки: above(kz, world, 2023) работает так же, но читающий должен помнить, чем был третий аргумент. Имя в вызове экономит это усилие.
Ловушка: into=[] в заголовке функции
Значение по умолчанию вычисляется один раз, когда Python читает def, а не при каждом вызове. Для числа это незаметно, для списка — беда:
def bad(value, into=[]):
into.append(value)
return into
bad(1) → [1]
bad(2) → [1, 2] ← тот же список, что и в прошлый раз
Второй вызов получил список с чужим значением внутри. Правильно так:
def good(value, into=None):
if into is None:
into = []
into.append(value)
return into
good(1) → [1]
good(2) → [2]
Правило простое: в заголовке функции по умолчанию стоит только неизменяемое — число, строка, None. Список, словарь и множество создают внутри.
assert: остановиться там, где ошибка появилась
assert count > 0, "в ряду нет ни одного числа"
assert проверяет то, во что вы верите, и если это неправда — останавливает программу с вашим текстом:
AssertionError: пустой ряд
Без него average на пустом ряде упала бы делением на ноль этажом ниже — с сообщением про деление, а не про данные. assert ловит ошибку там, где она возникла, и говорит человеческими словами.
И важная оговорка. assert — проверка ваших предположений о расчёте, а не проверка того, что прислал пользователь: запуск python -O их выключает целиком. Данные из чужого файла проверяют обычным if и понятной ошибкой; assert стоит там, где «этого не может быть, а если может — я хочу знать сразу».
lambda и key=: обещание из урока о списках
top = sorted(pairs, key=lambda pair: pair[1], reverse=True)
В уроке про списки мы сортировали кортежи через itemgetter и обещали написать свою функцию. Вот она: lambda pair: pair[1] — функция без имени, которая берёт пару и отдаёт из неё второй элемент. sorted вызывает её для каждого элемента и сортирует по тому, что она вернула.
То же самое можно написать через def, и для чего-то длиннее одной строки так и делают. lambda уместна ровно там, где функция короче своего имени.
Сколько угодно аргументов: *args и **kwargs
Иногда заранее неизвестно, сколько значений придёт:
def report(title, *values, **options):
digits = options.get("digits", 1)
out = []
for value in values:
out.append(f"{value:.{digits}f}")
return f"{title}: " + ", ".join(out)
report("инфляция", 8.0, 15.0, 14.5) → инфляция: 8.0, 15.0, 14.5
report("инфляция", 8.0, 15.0, digits=2) → инфляция: 8.00, 15.00
Звёздочка собирает лишние позиционные аргументы в кортеж, две звёздочки — именованные в словарь. Имена args и kwargs — только традиция; работают звёздочки.
Пользоваться этим в своём коде нужно редко, а вот читать — часто: так устроены половина функций в чужих библиотеках.
Имя внутри функции — своё
count = 0
def bump():
count = count + 1
bump()
→ UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
Присваивание внутри функции создаёт новое, местное имя. Python видит count = ... в теле и считает count местным во всей функции — включая строку, где его пытаются прочитать до присваивания.
Слово global это отключает, но правильный ответ почти всегда другой: принять значение аргументом и вернуть результат. Функция, которая ничего не трогает снаружи, проверяется одним вызовом — и это половина смысла сегодняшнего урока.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Что вернёт функция, в которой нет
return? - Почему
into=[]в заголовке функции — ошибка, аinto=None— нет? - Чем
assertотличается от проверки данных черезif?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает эта программа?
def average(series):
total = 0.0
count = 0
for value in series:
if value is None:
continue
total += value
count += 1
return total / count
print(average([8.0, None, 15.0]))
2. Заполните пропуск. Вместо ... поставьте условие: год берут, если число есть и оно больше предела.
def above(series, limit):
years = []
for year, value in series.items():
if ...:
years.append(year)
return years
print(above({2024: 8.7, 2025: 11.4, 2026: None}, 10))
3. Почините. Программа печатает число, а потом None. Объясните почему и исправьте так, чтобы вторая строка показала среднее.
def average(series):
print(sum(series) / len(series))
result = average([8.0, 15.0])
print("среднее:", result)
Задание
Обязательное. Дано:
kz = {2023: 14.5, 2024: 8.7, 2025: 11.4, 2026: None}
world = {2023: 5.8, 2024: 3.0, 2026: 2.9}
Заведите average(series) — среднее по ряду, пропуски не в счёт, пустой ряд останавливает assert с понятным текстом. Заведите above(series, other) — годы, в которых первый ряд выше второго; год без числа хоть в одном ряду не в счёт. Напечатайте среднее по каждому ряду с двумя знаками и список годов.
Ожидаемый вывод:
среднее по Казахстану: 11.53
среднее по миру: 3.90
выше мировой: [2023, 2024]
Готово, когда: вывод совпадает построчно; average вызвана дважды, а не написана дважды; above не падает на 2026 годе, где None с обеих сторон, и не берёт 2025-й, которого нет во втором ряду.
На своих данных. Заведите функцию average(series) для своего ряда чисел из прошлого урока: пропуски не в счёт, пустой ряд останавливает assert с понятным текстом. Заведите вторую — above(series, limit) — которая возвращает список ключей, где значение больше limit, и вызовите её дважды с разными пределами.
Всё это собрано в step-3 — сверьтесь после того, как напишете сами.
По желанию.
- Добавьте
averageаргументdigits=2и округляйте результат. - Отсортируйте свои пары через
sorted(key=lambda ...)по значению и по ключу. - Напишите функцию с
into=[], вызовите её трижды и посмотрите, что накопилось.
Куда это встанет в проекте
Сводка получает первые имена: «среднее по ряду», «годы выше нормы». Дальше они переедут в отдельный файл и станут модулем, а расчёт в главной программе останется в три строки.
Долги. Проверка у нас пока одна и стоит внутри расчёта. Настоящие проверки — отдельно от кода, который они проверяют; до pytest мы дойдём в конце курса, но assert в нужном месте работает уже сегодня.
Ответы
Показать ответы
На вопросы
None. Функция безreturnвыполняется до конца и возвращает пустоту — не ошибку, поэтому забытыйreturnвиден не сразу.- Потому что значение по умолчанию вычисляется один раз, при чтении
def. Список, созданный тогда, живёт между вызовами и копит в себе чужие значения;Noneнеизменяем, а новый список создаётся внутри при каждом вызове. assertпроверяет предположение автора о расчёте и выключается запускомpython -O. Данные, пришедшие снаружи, проверяют обычнымifс понятной ошибкой — на них полагаться нельзя.
К разминке
11.5. Пропуск не попал ни в сумму, ни в счётчик, поэтому делится23.0на2, а не на3.
11.5
value is not None and value > limit. Порядок важен: поставьте сравнение первым — и на пропуске программа упадёт, потому чтоNone > 10сравнить нельзя.
def above(series, limit):
years = []
for year, value in series.items():
if value is not None and value > limit:
years.append(year)
return years
print(above({2024: 8.7, 2025: 11.4, 2026: None}, 10))
[2025]
- Функция печатает, но ничего не возвращает, а функция без
returnвозвращаетNone— его и получилresult. Печать и возврат — разные вещи: печать показывает человеку,returnотдаёт программе.
def average(series):
return sum(series) / len(series)
result = average([8.0, 15.0])
print("среднее:", result)
среднее: 11.5
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.