Skip to content

Глава 9. Функции: свой набор инструментов аналитика

Задача главы: вынести повторяющиеся расчёты в собственные функции, передавать данные аргументами, возвращать результат через return и собрать первый переиспользуемый набор инструментов аналитика.


9.1 def: своя функция расчёта среднего балла

Зачем это нужно. Средний балл в этой книге уже считался вручную десяток раз — через sum() / len() или через накопители из главы Циклы. Если логику расчёта нужно поправить (например, округление), придётся искать все места, где она скопирована. Функция решает это раз и навсегда: логика написана один раз, у неё есть имя, и вызывается она откуда угодно.

// PYTHON_EXECUTOR (Pyodide Wasm)

Строка def calculate_average(scores): только описывает функцию — тело не выполняется, пока функцию не вызовут. scores внутри def — не тот же список, что group_scores снаружи, а параметр: временное имя, в которое при вызове calculate_average(group_scores) подставляется конкретный список. Вызов — это имя(аргументы), круглые скобки обязательны.

⚠️ Подводный камень

Первая ловушка — вызов раньше определения. Python выполняет файл построчно сверху вниз: пока def не отработал, имени calculate_average не существует.

// PYTHON_EXECUTOR (Pyodide Wasm)
Traceback (most recent call last):
File "report.py", line 2, in <module>
print(calculate_average(group_scores))
NameError: name 'calculate_average' is not defined

Вторая ловушка — забытые круглые скобки при вызове.

// PYTHON_EXECUTOR (Pyodide Wasm)
<function calculate_average at 0x7f8b2c0a1080>

Traceback не появится — Python не считает это ошибкой: без скобок имя функции ведёт себя как любое другое имя переменной и просто ссылается на объект-функцию, а не на результат её работы.

ОшибкаЧто значит для PythonЧеловеческий перевод
NameError«Имя calculate_average ещё не определено»Вызов стоит выше def в файле. Все функции нужно определять до первого использования — обычно их собирают в начале скрипта.
— (тихая ошибка)«Без () это ссылка на функцию, а не вызов»result = calculate_average присвоил переменной сам объект-функцию. Дальнейшая арифметика с result упадёт с TypeError, а до этого момента ошибка никак не проявляется.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)
Средний балл: 74.7

Пояснение логики: def собирают в начале файла (или в отдельном модуле — до главы Стандартная библиотека это будет просто верхняя часть скрипта), а вызовы — уже после, там, где нужен результат. Скобки () — это команда «выполни тело функции сейчас и отдай то, что она вернёт»; без них Python отдаёт саму функцию как объект.


9.2 Аргументы: позиционные, именованные, значения по умолчанию

Зачем это нужно. Функции редко работают с одним и тем же порогом всегда: экзамен может требовать 60 баллов для зачёта, а профильный — 75. Аргументы со значением по умолчанию позволяют не передавать порог каждый раз, но оставляют возможность переопределить его для особого случая.

// PYTHON_EXECUTOR (Pyodide Wasm)
зачёт
незачёт
зачёт

score — обязательный позиционный аргумент, без него будет TypeError. pass_threshold=60 — необязательный: если его не передать, используется 60; если передать вторым позиционным (get_status(87, 90)) или по имени (pass_threshold=90) — используется переданное значение. Именованные аргументы можно передавать в любом порядке — Python сопоставляет их по имени, а не по позиции.

⚠️ Подводный камень

Изменяемый объект в значении по умолчанию — та же ловушка, что подстерегала в setdefault() из главы Словари, только там она пряталась в аргументах метода словаря, а здесь — в аргументах собственной функции. Значение по умолчанию вычисляется один раз, в момент определения функции, а не при каждом вызове.

// PYTHON_EXECUTOR (Pyodide Wasm)
[87]
[87, 45]

Оба вызова без второго аргумента получают ссылку на один и тот же список — тот, что был создан один раз при определении функции. Второй вызов дописывает в список первого, а не начинает новый.

ОшибкаЧто значит для PythonЧеловеческий перевод
— (тихая ошибка)«Значение по умолчанию — один объект на все вызовы»scores=[] — не «шаблон нового списка на каждый вызов», а конкретный список, созданный один раз. Между вызовами без явного аргумента он копит данные, как empty_list, переданный в setdefault() из главы 5.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)
[87]
[45]

Пояснение логики: значением по умолчанию должен быть неизменяемый объект — число, строка, None. Если по смыслу нужен изменяемый объект (список, словарь), по умолчанию ставят None, а сам объект создают внутри тела функции — тогда каждый вызов честно получает новый, независимый экземпляр.


9.3 return: возврат значения против print() внутри функции

Зачем это нужно. print() показывает значение человеку на экране, а return передаёт значение обратно в код, который вызвал функцию, — чтобы использовать его дальше: сложить, сравнить, подставить в другую функцию. Если функция должна участвовать в дальнейших расчётах, print() внутри неё результат не заменяет.

// PYTHON_EXECUTOR (Pyodide Wasm)
Иванов Иван: 87 б. — зачёт

⚠️ Подводный камень

Функция без return (или с return без значения) молча отдаёт None — Python не считает это ошибкой на уровне самой функции, но всё ломается там, где None пытаются использовать как число.

// PYTHON_EXECUTOR (Pyodide Wasm)
74.66666666666667
Traceback (most recent call last):
File "report.py", line 7, in <module>
bonus_score = average_score + 5
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
ОшибкаЧто значит для PythonЧеловеческий перевод
TypeError«Нельзя сложить NoneType и int»Функция напечатала результат через print(), но не отдала его через return. Любая функция без явного return возвращает None — именно это None и попало в average_score.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)
Средний балл с бонусом: 79.7

Пояснение логики: правило простое — если результат функции нужен где-то ещё в коде, используйте return; если функция — последний шаг и её задача только показать что-то человеку, print() внутри уместен, но тогда результат этой функции никуда дальше не передают. Смешивать оба назначения в одной функции — источник именно этой ловушки.


9.4 Область видимости: локальные и глобальные имена

Зачем это нужно. Переменная, созданная внутри функции, — локальная: она существует только во время вызова и не видна снаружи. Это удобно — функции не мешают друг другу одинаковыми именами параметров. Но у этого правила есть обратная сторона, обещанная ещё в мини-словарике главы Циклы: изменить снаружи заданный накопитель изнутри функции не получится так же просто, как прочитать его.

// PYTHON_EXECUTOR (Pyodide Wasm)
зачёт

Читать глобальную переменную изнутри функции можно свободно — pass_threshold внутри get_status берётся из внешней области, потому что нигде в теле функции ей ничего не присваивается.

⚠️ Подводный камень

Как только внутри функции переменной с тем же именем, что и снаружи, присваивают значение — даже через += — Python на этапе разбора всей функции считает это имя локальным на всё тело целиком, включая строки выше присваивания. Внешнее значение перестаёт быть видно.

// PYTHON_EXECUTOR (Pyodide Wasm)
Traceback (most recent call last):
File "report.py", line 6, in <module>
add_to_total(87)
File "report.py", line 4, in add_to_total
total_score += score
UnboundLocalError: local variable 'total_score' referenced before assignment
ОшибкаЧто значит для PythonЧеловеческий перевод
UnboundLocalError«Локальная переменная использована до присваивания»total_score += score внутри функции — это присваивание, значит Python считает total_score локальным именем на всю функцию. Но += сначала пытается прочитать текущее значение, а такой локальной переменной ещё нет — отсюда ошибка. Внешний total_score из-за этого правила не виден вовсе, хотя обычное чтение (без присваивания) сработало бы.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)
132
// PYTHON_EXECUTOR (Pyodide Wasm)
87

Пояснение логики: вариант А — данные входят и выходят через аргументы и return, у функции нет скрытых побочных эффектов, её легко проверить отдельно от остального скрипта. Вариант Б работает, но global прячет от читателя код тот факт, что вызов функции меняет что-то за её пределами — в большом скрипте такие скрытые изменения труднее отследить. Предпочтение — вариант А; global держат в уме как исключение для редких случаев.


9.5 Мини-словарик ошибок девятой главы

ОшибкаЧто значит для PythonЧеловеческий перевод
NameError«Имя функции ещё не определено»Вызов стоит в коде раньше, чем def. Все функции определяют до первого вызова.
TypeError (арифметика с None)«Нельзя сложить NoneType и число»Функция напечатала результат через print(), но не вернула его через return — снаружи оказался None.
UnboundLocalError«Локальная переменная использована до присваивания»Внутри функции переменной с именем внешнего накопителя присвоили значение (в том числе через +=) — Python сделал имя локальным на всю функцию и потерял доступ к внешнему значению.
— (тихая ошибка)«Без () — это ссылка на функцию, а не вызов»Забыли скобки при вызове: переменная получила сам объект-функцию, а не результат её работы.
— (тихая ошибка)«Значение по умолчанию — один объект на все вызовы»Изменяемый объект (scores=[]) в значении по умолчанию создаётся один раз при определении функции и копит данные между вызовами без явного аргумента.

Отдельного разбора заслуживает ошибка при неверном числе аргументов — она встречается на практике едва ли не чаще всех прочих:

// PYTHON_EXECUTOR (Pyodide Wasm)
Traceback (most recent call last):
File "report.py", line 4, in <module>
print(get_status(87))
TypeError: get_status() missing 1 required positional argument: 'pass_threshold'

Python точно называет недостающий аргумент по имени — это самая говорящая трассировка в книге на данный момент: достаточно передать pass_threshold вторым аргументом или дать ему значение по умолчанию (pass_threshold=60), как в 9.2, чтобы аргумент стал необязательным.


Мини-кейс: модуль расчётов — набор функций аналитика

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

// PYTHON_EXECUTOR (Pyodide Wasm)
--- ОТЧЁТ ПО ГРУППЕ ---
Иванов Иван: средний балл 71.7 — зачёт
Смирнова Ольга: средний балл 91.7 — зачёт
Ким Юрий: средний балл 48.3 — незачёт
--- Сдали: 2 из 3 ---

Что получилось: каждая функция отвечает ровно за один шаг — расчёт, оценку статуса или сборку строки — и ни одна не печатает результат сама, все возвращают значение через return. Цикл из главы Циклы использует их как готовые инструменты, поэтому логику расчёта достаточно поменять в одном месте (calculate_average), а не искать копии формулы по всему скрипту.

⚠️ Если у ученика в выгрузке окажется пустой список scores (не сдал ни одного модуля), calculate_average() упадёт с ZeroDivisionError — та же защита len(...) > 0, что и в главе 8, здесь просто спрятана внутрь функции. Устойчивая обработка такой строки выгрузки, без остановки всего отчёта, — тема главы Ошибки и исключения.


Чек-лист главы

  • Определяю функцию через def и вызываю её только после того, как определение уже выполнено в коде
  • Не забываю круглые скобки при вызове — без них получаю ссылку на функцию, а не результат
  • Передаю данные в функцию через аргументы, а не через глобальные переменные
  • Задаю значения по умолчанию для необязательных аргументов (pass_threshold=60)
  • Не использую изменяемый объект (список, словарь) как значение по умолчанию — беру None и создаю объект внутри функции
  • Возвращаю результат через return, если он нужен дальше по коду, а не только печатаю его внутри функции
  • Помню, что функция без return отдаёт None, и это ломает дальнейшую арифметику TypeError
  • Понимаю, что переменные, созданные внутри функции, — локальные и не видны снаружи
  • Не меняю внешний накопитель оператором += внутри функции — передаю и возвращаю значение явно
  • Узнаю UnboundLocalError как сигнал: присваивание внутри функции сделало имя локальным на всю функцию
  • Узнаю TypeError: missing ... required positional argument как сигнал нехватки аргумента при вызове
  • Собираю переиспользуемые функции в начале скрипта — как личный набор инструментов аналитика

Дальше: Файлы и CSV