Функции: как перестать повторять себя и навести порядок
К этому моменту мы умеем хранить данные (переменные), выбирать между вариантами (условия) и повторять действия (циклы). Этого достаточно, чтобы написать работающую программу — но не достаточно, чтобы написать программу, которую приятно читать и не страшно менять. Сейчас мы возьмём последний из четырёх базовых инструментов. Он называется функция, и это, пожалуй, самая полезная идея во всём начальном программировании.
Проблема, из которой всё выросло
Представьте магазин. Нужно посчитать итоговую цену товаров: к цене прибавляется НДС 20%, а если сумма больше 5000 — скидка 10%. Без функций это выглядит так:
# товар 1
price = 3000
with_tax = price * 1.2
if with_tax > 5000:
with_tax = with_tax * 0.9
print("Товар 1:", with_tax)
# товар 2
price = 7000
with_tax = price * 1.2
if with_tax > 5000:
with_tax = with_tax * 0.9
print("Товар 2:", with_tax)
Программа работает. И всё же с ней плохо. Одна и та же логика написана дважды (а в реальном магазине — двадцать раз). И если завтра НДС станет 22%, придётся найти и исправить все эти места. Найдёте девятнадцать, пропустите одно — и программа начнёт врать, не падая и никак вас не предупреждая. Такие ошибки самые дорогие: их не видно.
У этой боли есть имя. Принцип DRY — Don’t Repeat Yourself, «не повторяй себя»: у каждого знания в программе должно быть ровно одно место, где оно записано. Сформулировали его Энди Хант и Дэйв Томас в книге The Pragmatic Programmer. Функция — главный инструмент, которым DRY достигается.
Аналогия: рецепт
Функция — это записанный рецепт. Один раз вы записываете: «Блины: взять столько-то муки, столько-то молока, сделать то-то, получить стопку блинов». Записали — ничего ещё не приготовили, на кухне пусто. Но теперь вместо того чтобы каждый раз вспоминать весь порядок действий, вы говорите: «делаю блины по рецепту, мука такая-то» — и получаете результат.
Здесь сразу видны все части функции: название рецепта, чтобы к нему обращаться; ингредиенты, которые надо подставить, — они называются параметрами; сами шаги — тело функции; и готовое блюдо, которое рецепт отдаёт, — возвращаемое значение.
Где аналогия ломается. Рецепт можно выполнить «примерно» — на глаз, чуть иначе. Функция всегда выполняется буквально и одинаково: те же аргументы — тот же результат. И ещё: повар может по ходу заглянуть в холодильник и передумать, а функция видит только то, что ей передали (об этом будет отдельный раздел).
Первая функция
def greet():
print("Привет!")
Запустите этот файл. На экране — ничего. Это не поломка: мы записали рецепт, но не готовили по нему. Разберём построчно:
def— от английского define, «определить». Ключевое слово, с которого начинается любая функция.greet— имя, которое мы придумали сами. По нему функцию потом вызывают.()— круглые скобки. Сейчас пустые, скоро в них появятся параметры. Скобки обязательны, даже пустые.:— двоеточие, как вifиfor. Означает «дальше идёт блок».print("Привет!")— тело функции, сдвинутое на четыре пробела. Отступ и здесь работает как граница блока.
Чтобы функция что-то сделала, её нужно вызвать — написать имя и скобки:
def greet():
print("Привет!")
greet()
greet()
Вывод:
Привет!
Привет!
Обратите внимание на порядок: сначала объявление, потом вызовы. Python читает файл сверху вниз, и на момент вызова функция уже должна быть ему известна.
Параметры: как передать функции данные
Функция, которая всегда делает одно и то же, полезна мало. Настоящая сила появляется, когда в неё можно что-то передать.
def greet(name):
print("Привет, " + name + "!")
greet("Аня")
greet("Борис")
Вывод:
Привет, Аня!
Привет, Борис!
name внутри скобок — это параметр: пустая ячейка, имя без значения. Значение появится в момент вызова. "Аня" в строке greet("Аня") — это аргумент: конкретное значение, которое встаёт в эту ячейку.
Параметр — что написано в объявлении. Аргумент — что реально передали при вызове. Различие пригодится при чтении сообщений об ошибках: Python использует эти слова буквально.
Return: как получить результат обратно
Пока наши функции только печатали. Гораздо чаще функция должна посчитать и отдать значение — чтобы с ним можно было работать дальше.
def area(width, height):
result = width * height
return result
s = area(3, 4)
print(s) # 12
print(area(5, 5)) # 25
print(area(3, 4) + area(1, 2)) # 14
return делает две вещи одновременно: немедленно завершает функцию (всё, что написано после него, не выполнится — это частая причина «почему мой print не сработал») и отдаёт значение наружу, в то место, откуда функцию вызвали.
Вот полная картина одного вызова: программа приостанавливается, управление уходит внутрь функции, а потом возвращается ровно в ту же точку — уже со значением.
считаем 3 * 4 = 12 F-->>M: return 12 Note over M: s = 12
продолжаем со следующей строки
return и print — это НЕ одно и то же
Здесь спотыкаются буквально все, поэтому остановимся подробно. print — показать человеку: текст появляется на экране и исчезает из жизни программы. return — отдать программе: на экране ничего не появляется, но значение можно положить в переменную, сложить, передать дальше.
def area_print(w, h):
print(w * h) # показываем на экране
def area_return(w, h):
return w * h # отдаём программе
a = area_print(3, 4) # на экране появится 12
b = area_return(3, 4) # на экране НЕ появится ничего
print(a) # None
print(b) # 12
Вывод целиком:
12
None
12
Первая 12 — это print изнутри area_print. Дальше print(a) выводит None, потому что area_print ничего не вернула. И только print(b) печатает настоящий результат.
None — это специальное значение в Python, означающее «ничего», «пусто». Функция без return возвращает именно None. Если вы видите неожиданное None — почти наверняка забыли return.
Правило, которое стоит взять за привычку: функция считает и возвращает, печатает — тот, кто её вызвал. Так одну и ту же функцию можно использовать и в консольной программе, и в вебе, и в тесте.
Несколько return: ранний выход
return можно поставить в нескольких местах — сработает первый, до которого дойдёт выполнение.
def check_age(age):
if age < 0:
return "Так не бывает"
if age < 18:
return "Несовершеннолетний"
return "Совершеннолетний"
print(check_age(-5)) # Так не бывает
print(check_age(12)) # Несовершеннолетний
print(check_age(30)) # Совершеннолетний
Заметьте: else не понадобился. Если условие сработало — функция уже завершилась, до следующих строк дело не дойдёт. Такой приём называется ранний выход (early return) и делает код заметно чище, чем лестница вложенных if.
Значения по умолчанию и именованные аргументы
Параметру можно задать значение, которое подставится, если аргумент не передали:
def greet(name, greeting="Привет"):
return greeting + ", " + name + "!"
print(greet("Аня")) # Привет, Аня!
print(greet("Аня", "Доброе утро")) # Доброе утро, Аня!
Параметры со значением по умолчанию идут после обычных — иначе Python не поймёт, что куда подставлять, и выдаст SyntaxError: non-default argument follows default argument.
Аргументы можно передавать по имени — тогда порядок не важен, и код читается лучше:
def order(product, count=1, express=False):
return f"{product}: {count} шт., срочно={express}"
print(order("Кофе"))
print(order("Кофе", count=3))
print(order("Кофе", express=True)) # count останется 1
print(order(count=2, product="Чай")) # порядок другой — работает
Вывод:
Кофе: 1 шт., срочно=False
Кофе: 3 шт., срочно=False
Кофе: 1 шт., срочно=True
Чай: 2 шт., срочно=False
f"..." — это f-строка (форматированная строка): всё, что стоит в фигурных скобках, вычисляется и подставляется в текст.
Ловушка, о которой лучше узнать сразу. Значением по умолчанию не должен быть список или словарь (разберём их в следующей статье). Такое значение создаётся один раз при объявлении функции и потом живёт между вызовами, накапливая мусор. Правильный приём — поставить None и создать список внутри:
def add_item(item, basket=None):
if basket is None:
basket = [] # новый пустой список на каждый вызов
basket.append(item)
return basket
Пока запомните это как рецепт — почему именно так, станет очевидно после статьи о списках.
Область видимости: у функции своя комната
Переменные, созданные внутри функции, снаружи не существуют.
def calc():
secret = 42 # локальная переменная
return secret
print(calc()) # 42 — вернули значение, всё хорошо
print(secret) # NameError: name 'secret' is not defined
Это не ограничение, а подарок: именно благодаря ему можно писать функции, не держа в голове весь остальной файл — имена внутри не столкнутся с чужими именами снаружи.
Обратное направление работает: переменную, объявленную снаружи, внутри функции прочитать можно.
TAX = 0.2 # глобальная переменная
def with_tax(price):
return price * (1 + TAX) # TAX читается спокойно
print(with_tax(1000)) # 1200.0
А вот изменить глобальную переменную изнутри просто так нельзя:
count = 0
def increase():
count = count + 1 # ошибка!
increase()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
Дословно: «не могу обратиться к локальной переменной count — ей ещё не присвоено значение». (В Python до версии 3.11 та же ошибка звучала как local variable 'count' referenced before assignment.) Логика Python такая: раз внутри функции есть присваивание count = ..., значит count объявляется локальной; а в правой части мы пытаемся прочитать её до того, как в неё что-то положили.
Существует слово global, которое снимает запрет, но пользоваться им почти никогда не нужно. Правильное решение — передать значение аргументом и вернуть результат:
def increase(value):
return value + 1
count = 0
count = increase(count)
print(count) # 1
Функция, которая берёт данные только из аргументов и отдаёт только через return, называется чистой (pure). Такие функции легче всего понимать и проверять: чтобы разобраться в чистой функции, достаточно смотреть на неё саму.
Функции меняют форму программы
Вернёмся к магазину из начала статьи и перепишем его.
TAX = 0.2
DISCOUNT_LIMIT = 5000
DISCOUNT = 0.1
def final_price(price):
"""Считает итоговую цену: НДС, затем скидка для крупных покупок."""
with_tax = price * (1 + TAX)
if with_tax > DISCOUNT_LIMIT:
with_tax = with_tax * (1 - DISCOUNT)
return round(with_tax, 2)
print("Товар 1:", final_price(3000))
print("Товар 2:", final_price(7000))
print("Товар 3:", final_price(1200))
Вывод:
Товар 1: 3600.0
Товар 2: 7560.0
Товар 3: 1440.0
Логика записана один раз, новый НДС меняется в одном месте, а название final_price объясняет смысл лучше любого комментария. Строка в тройных кавычках сразу после def — это докстринг (docstring), встроенная документация: её показывает help(final_price) и подсказки редактора. Правила оформления — в PEP 257.
Экономия строк приятна, но второстепенна; важнее то, что программа перестаёт быть лентой команд и превращается в набор понятий, у каждого из которых есть имя.
Как разбивать программу на функции
Общее правило: одна функция — одна задача, которую можно назвать одним глаголом. Если в названии просится «и» (load_and_print_and_save), функций должно быть три. Типичная структура небольшой программы выглядит так:
спросить данные"] MAIN --> CALC["calculate(data)
посчитать"] MAIN --> SHOW["show_result(value)
показать"] CALC --> V["validate(data)
проверить корректность"]
Читая main(), вы за пять секунд понимаете, что делает программа, — не вникая, как именно. Захотели подробностей — спустились в нужную функцию. Это называется уровнями абстракции; подробно об этом написано в главе о функциях книги Роберта Мартина Clean Code. Практические ориентиры для начинающих:
- Длина. Если функция не помещается на экран (примерно 20–30 строк) — скорее всего, внутри прячется вторая функция.
- Имя. Функция — это действие, поэтому имя обычно глагол:
calculate_total,send_email. Функции, отвечающие «да/нет», принято называть сis_илиhas_. Стиль имён в Python — маленькими буквами через подчёркивание (PEP 8). - Количество параметров. Три-четыре — это уже много: длинный список параметров говорит, что функция делает слишком многое.
- Не бойтесь коротких функций. Функция из двух строк с хорошим именем полезнее, чем те же две строки, повторённые в четырёх местах.
Типичные ошибки новичков и что означают сообщения
1. Объявили, но не вызвали
Программа завершается молча, ошибки нет — просто нет вызова. Добавьте greet() отдельной строкой без отступа.
2. Забыли скобки при вызове
Строка greet без скобок не делает ничего и не считается ошибкой. А print(greet) выведет что-то вроде:
<function greet at 0x7f3a2c0d5f70>
Дословно: «объект-функция greet, лежащий в памяти по адресу…». Имя без скобок — это сама функция как объект; скобки означают «выполнить её».
3. Передали не столько аргументов
def greet(name):
print("Привет, " + name)
greet() # TypeError: greet() missing 1 required
# positional argument: 'name'
greet("Аня", "Борис") # TypeError: greet() takes 1 positional
# argument but 2 were given
Первое сообщение: «отсутствует 1 обязательный позиционный аргумент: name». Позиционный — значит определяемый по своему месту в скобках. Второе: «функция принимает 1 позиционный аргумент, а передано 2». Смотрите на объявление и на вызов — количества должны совпасть.
4. Функция печатает вместо того, чтобы возвращать
def double(x):
print(x * 2)
result = double(5)
print(result + 1)
10
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
«Операция + не поддерживается для типов NoneType и int». На человеческий: вы пытаетесь прибавить единицу к «ничему». Функция напечатала результат, но не вернула его. Замените print(x * 2) на return x * 2.
5. Вызов раньше объявления
greet()
def greet():
print("Привет!")
NameError: name 'greet' is not defined
«Имя greet не определено» — на момент вызова Python ещё не дошёл до строки с def. Обычно все def пишут сверху файла, а сам сценарий — внизу. Та же ошибка возникает при опечатке в имени.
6. Забыли двоеточие или отступ
def greet() # SyntaxError: expected ':'
print("Привет!")
def greet():
print("Привет!") # IndentationError: expected an indented block
# after function definition on line 1
Второе сообщение дословно: «ожидался блок с отступом после объявления функции в строке 1». Сдвиньте тело на четыре пробела.
7. Имя функции совпало со встроенным
def print(text): # так делать не надо
...
Python разрешит, но настоящий print перестанет работать во всём файле, и найти причину будет непросто. Избегайте имён print, input, list, sum, type, str, int, len, max, min — полный список в документации.
8. Ждали изменения переменной снаружи
def reset(value):
value = 0
x = 10
reset(x)
print(x) # 10, а не 0
Функция переприсвоила имя value у себя внутри — снаружи ничего не изменилось. Верните значение и присвойте его явно: x = reset(x).
Функции, которые уже написаны за вас
Вы пользовались функциями с самого начала: print(), input(), len(), int(), round() — это всё встроенные функции. Ещё больше готовых лежит в модулях — файлах с кодом, которые можно подключить:
import math # подключаем модуль математики
print(math.sqrt(16)) # 4.0 — квадратный корень
print(math.floor(3.7)) # 3 — округление вниз
print(math.pi) # 3.141592653589793
import math означает «дай мне доступ к модулю math», а math.sqrt читается как «функция sqrt из модуля math». Список всего, что есть в стандартной библиотеке, — в официальной документации; он огромный, и заучивать его не нужно. Мысль, к которой стоит привыкнуть: прежде чем писать функцию, полезно проверить, не написана ли она уже.
Практика
Каждое задание пишите в отдельном файле и обязательно запускайте — функции лучше всего осваиваются на пальцах.
1. Приветствие
Напишите функцию greet(name), которая возвращает (не печатает!) строку вида Привет, Аня!. Вызовите её три раза с разными именами и напечатайте результаты.
Подсказка: внутри — одна строка с return и f-строкой: return f"Привет, {name}!".
2. Максимум из трёх
Напишите функцию max_of_three(a, b, c), возвращающую наибольшее из трёх чисел. Встроенным max() пользоваться нельзя — в этом весь смысл упражнения.
Подсказка: сначала найдите больший из a и b, запомните его в переменную, потом сравните её с c. Проверьте на случаях, где числа равны и где максимум стоит в начале, в середине и в конце.
3. Конвертер температуры
Напишите celsius_to_fahrenheit(c) и обратную ей fahrenheit_to_celsius(f). Формулы: f = c * 9 / 5 + 32 и c = (f - 32) * 5 / 9. Проверьте себя так: переведите 100 градусов в фаренгейты и результат — обратно. Должно получиться примерно 100.
Подсказка: «примерно» — не оговорка: дробные числа хранятся с крошечной погрешностью, и точного равенства может не быть. Используйте round(value, 2) при выводе.
4. Проверка пароля
Напишите функцию is_password_strong(password), которая возвращает True или False. Пароль надёжен, если в нём не меньше 8 символов, есть хотя бы одна цифра и хотя бы одна буква. Затем напишите вторую функцию password_report(password), которая пользуется первой и возвращает готовый текст: «Пароль надёжный» или «Пароль слабый».
Подсказки: длина — len(password). Чтобы найти цифру, пройдите по символам циклом for и используйте symbol.isdigit() — он возвращает True для символов от 0 до 9; для букв есть symbol.isalpha(). Заведите две переменные-флага (has_digit = False) и переключайте их в True, когда нашли нужный символ. Обратите внимание на структуру: одна функция отвечает на вопрос, вторая формулирует ответ человеку — это и есть «одна функция, одна задача».
5. Со звёздочкой: калькулятор на функциях
Возьмите калькулятор из статьи об условиях и перепишите его на функциях: add(a, b), subtract(a, b), multiply(a, b), divide(a, b) и calculate(a, b, operation), которая выбирает нужную и возвращает результат. divide должна корректно обрабатывать деление на ноль.
Подсказка: пусть divide при нулевом делителе возвращает None, а calculate проверяет результат перед выдачей. Так проверка на ошибку остаётся внутри той функции, которая про эту ошибку знает.
Мини-итог
- Функция — записанный один раз кусок работы, у которого есть имя. Объявляется через
def, выполняется только при вызове со скобками. - Параметр — имя в объявлении, аргумент — значение при вызове.
returnвозвращает значение в программу и немедленно завершает функцию.printпоказывает текст человеку и ничего не возвращает. Это разные вещи.- Функция без
returnвозвращаетNone. НеожиданноеNoneв выводе — почти всегда забытыйreturn. - У параметров бывают значения по умолчанию; аргументы можно передавать по имени. Списки и словари значением по умолчанию ставить нельзя — используйте
None. - Переменные внутри функции снаружи не видны. Читать глобальные можно, менять — не нужно: передавайте аргументом, возвращайте результат.
- Одна функция — одна задача, имя-глагол, желательно в пределах экрана.
TypeError: missing 1 required positional argument— забыли аргумент.NameError: name 'x' is not defined— вызвали до объявления или опечатались в имени.
Дополнительно почитать: раздел «Defining Functions» в официальном учебнике Python и главу о функциях в Automate the Boring Stuff with Python Эла Свейгарта — она бесплатно доступна онлайн и написана специально для новичков.
Что дальше
Теперь у нас есть все четыре опоры: переменные, условия, циклы и функции. Не хватает последнего — умения работать не с одним значением, а с наборами: списком покупок, таблицей сотрудников, набором уникальных тегов. Этим займёмся дальше.