Shanraq.org Shanraq.org
Функции: имя для расчёта и assert, который ловит неверное число
IT

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 даёт не ошибку, а пустоту в расчёте, и ищут её потом долго.

Строка в тройных кавычках сразу после defdocstring, описание функции. Мы уже видели его в начале файлов; здесь он объясняет не программу, а один кусок, и это то место, где стоит написать, что функция делает с пропусками.

Сказать — мало, надо и сделать. В 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 это отключает, но правильный ответ почти всегда другой: принять значение аргументом и вернуть результат. Функция, которая ничего не трогает снаружи, проверяется одним вызовом — и это половина смысла сегодняшнего урока.

Карта урока

Карта урока: имя, аргумент и проверка

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

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

  1. Что вернёт функция, в которой нет return?
  2. Почему into=[] в заголовке функции — ошибка, а into=None — нет?
  3. Чем 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 в нужном месте работает уже сегодня.

Ответы

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

На вопросы

  1. None. Функция без return выполняется до конца и возвращает пустоту — не ошибку, поэтому забытый return виден не сразу.
  2. Потому что значение по умолчанию вычисляется один раз, при чтении def. Список, созданный тогда, живёт между вызовами и копит в себе чужие значения; None неизменяем, а новый список создаётся внутри при каждом вызове.
  3. assert проверяет предположение автора о расчёте и выключается запуском python -O. Данные, пришедшие снаружи, проверяют обычным if с понятной ошибкой — на них полагаться нельзя.

К разминке

  1. 11.5. Пропуск не попал ни в сумму, ни в счётчик, поэтому делится 23.0 на 2, а не на 3.
11.5
  1. 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]
  1. Функция печатает, но ничего не возвращает, а функция без return возвращает None — его и получил result. Печать и возврат — разные вещи: печать показывает человеку, return отдаёт программе.
def average(series):
    return sum(series) / len(series)


result = average([8.0, 15.0])
print("среднее:", result)
среднее: 11.5

Источники

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

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

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

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

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

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