Программирование с нуля Функции: как перестать повторять себя и навести порядок
0%

Функции: как перестать повторять себя и навести порядок

Функции: как перестать повторять себя и навести порядок

К этому моменту мы умеем хранить данные (переменные), выбирать между вариантами (условия) и повторять действия (циклы). Этого достаточно, чтобы написать работающую программу — но не достаточно, чтобы написать программу, которую приятно читать и не страшно менять. Сейчас мы возьмём последний из четырёх базовых инструментов. Он называется функция, и это, пожалуй, самая полезная идея во всём начальном программировании.

Проблема, из которой всё выросло

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

У этой боли есть имя. Принцип DRYDon’t Repeat Yourself, «не повторяй себя»: у каждого знания в программе должно быть ровно одно место, где оно записано. Сформулировали его Энди Хант и Дэйв Томас в книге The Pragmatic Programmer. Функция — главный инструмент, которым DRY достигается.

Аналогия: рецепт

Функция — это записанный рецепт. Один раз вы записываете: «Блины: взять столько-то муки, столько-то молока, сделать то-то, получить стопку блинов». Записали — ничего ещё не приготовили, на кухне пусто. Но теперь вместо того чтобы каждый раз вспоминать весь порядок действий, вы говорите: «делаю блины по рецепту, мука такая-то» — и получаете результат.

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

Где аналогия ломается. Рецепт можно выполнить «примерно» — на глаз, чуть иначе. Функция всегда выполняется буквально и одинаково: те же аргументы — тот же результат. И ещё: повар может по ходу заглянуть в холодильник и передумать, а функция видит только то, что ей передали (об этом будет отдельный раздел).

Первая функция

def greet():
    print("Привет!")

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

  1. def — от английского define, «определить». Ключевое слово, с которого начинается любая функция.
  2. greet — имя, которое мы придумали сами. По нему функцию потом вызывают.
  3. () — круглые скобки. Сейчас пустые, скоро в них появятся параметры. Скобки обязательны, даже пустые.
  4. : — двоеточие, как в if и for. Означает «дальше идёт блок».
  5. 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 не сработал») и отдаёт значение наружу, в то место, откуда функцию вызвали.

Из чего состоит функция

Вот полная картина одного вызова: программа приостанавливается, управление уходит внутрь функции, а потом возвращается ровно в ту же точку — уже со значением.

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(), вы за пять секунд понимаете, что делает программа, — не вникая, как именно. Захотели подробностей — спустились в нужную функцию. Это называется уровнями абстракции; подробно об этом написано в главе о функциях книги Роберта Мартина 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 Эла Свейгарта — она бесплатно доступна онлайн и написана специально для новичков.

Что дальше

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

Списки, словари и множества: работа с наборами данных

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов