Вызов функций и стек вызовов в Python

Обновлено:

Функции в программе могут работать не только самостоятельно.

Одна функция может:

  • вызвать другую функцию;
  • передать ей аргументы;
  • получить от неё результат;
  • продолжить свою работу;
  • вернуть собственный результат дальше.

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


1. Простой пример

Создадим три функции:

def calculate_discount(price):
    return price * 0.1


def calculate_delivery(price):
    if price >= 5000:
        return 0

    return 500


def calculate_order_total(price):
    discount = calculate_discount(price)
    delivery = calculate_delivery(price)

    total_price = price - discount + delivery

    return total_price

Вызовем главную функцию:

result = calculate_order_total(4000)

print(result)

Результат:

4100.0

Здесь функции связаны между собой:

calculate_order_total()
    ↓
calculate_discount()

calculate_order_total()
    ↓
calculate_delivery()

Главная функция использует результаты двух других функций для собственного вычисления.


Последовательность выполнения

2. Что происходит при вызове

Начинаем с глобальной области:

result = calculate_order_total(4000)

Python переходит внутрь:

calculate_order_total()

и начинает выполнять её сверху вниз.

Первая операция:

discount = calculate_discount(price)

Для её выполнения необходимо вызвать другую функцию.

Поэтому работа calculate_order_total() временно приостанавливается.


3. Переход во вторую функцию

Python вызывает:

calculate_discount(4000)

Внутри выполняется:

return price * 0.1

Получаем:

400.0

После return функция calculate_discount() завершается.

Python возвращается именно туда, откуда она была вызвана:

discount = calculate_discount(price)

Поэтому фактически получается:

discount = 400.0

После этого calculate_order_total() продолжает работу со следующей строки.


4. Следующий вызов

Далее выполняется:

delivery = calculate_delivery(price)

Первая функция снова временно приостанавливается, и Python переходит в:

calculate_delivery(4000)

Для значения 4000 функция возвращает:

500

Python снова возвращается в:

calculate_order_total()

и получается:

delivery = 500

После этого выполняется:

total_price = price - discount + delivery

То есть:

4000 - 400 + 500

Получаем:

4100.0

В конце:

return total_price

возвращает результат уже из главной функции в глобальную область.


Цепочка из нескольких функций

5. Одна функция вызывает вторую, а вторая — третью

Рассмотрим более последовательный пример:

def calculate_tax(price):
    return price * 0.18


def calculate_price_with_tax(price):
    tax = calculate_tax(price)

    return price + tax


def create_price_report(price):
    final_price = calculate_price_with_tax(price)

    return f'Итоговая стоимость: {final_price}'


report = create_price_report(1000)

print(report)

Результат:

Итоговая стоимость: 1180.0

Здесь появляется цепочка:

глобальная область
        ↓
create_price_report()
        ↓
calculate_price_with_tax()
        ↓
calculate_tax()

6. Как выполняется такая цепочка

Начинаем здесь:

report = create_price_report(1000)

Шаг 1

Вызывается:

create_price_report(1000)

Внутри неё Python доходит до:

final_price = calculate_price_with_tax(price)

Теперь первая функция ждёт результат второй.

Шаг 2

Вызывается:

calculate_price_with_tax(1000)

Она доходит до:

tax = calculate_tax(price)

Теперь уже вторая функция ждёт результат третьей.

Шаг 3

Вызывается:

calculate_tax(1000)

Она выполняет:

return price * 0.18

и возвращает:

180.0

7. Возвращение происходит в обратном порядке

После завершения:

calculate_tax()

Python возвращается не сразу в глобальную область, а в функцию, которая её вызвала:

calculate_price_with_tax()

Получается:

tax = 180.0

Функция продолжает работу:

return price + tax

и возвращает:

1180.0

Теперь Python возвращается в:

create_price_report()

Получается:

final_price = 1180.0

Функция продолжает работу:

return f'Итоговая стоимость: {final_price}'

И только после этого результат возвращается в глобальную область:

report = 'Итоговая стоимость: 1180.0'

Схема выполнения

8. Вызовы идут в одну сторону, возвраты — в обратную

Вызовы:

Global
  ↓
create_price_report()
  ↓
calculate_price_with_tax()
  ↓
calculate_tax()

Возвраты:

calculate_tax()
  ↑
calculate_price_with_tax()
  ↑
create_price_report()
  ↑
Global

Можно представить это так:

Global
  ↓
Function 1
  ↓
Function 2
  ↓
Function 3

Function 3
  ↑
Function 2
  ↑
Function 1
  ↑
Global

Каждая функция после вызова другой функции ждёт её завершения.

После получения результата она продолжает выполнение с того места, где остановилась.


Стек вызовов

9. Call Stack

Такой механизм связан с понятием:

Call Stack

или:

стек вызовов

Стек вызовов хранит информацию о текущей цепочке вызванных функций.

Например:

create_price_report()
calculate_price_with_tax()
calculate_tax()

Когда вызывается новая функция, информация о её вызове добавляется в стек.

Когда функция завершается, её вызов убирается из стека, и Python возвращается к предыдущей функции.

Упрощённо:

1. create_price_report()
2. calculate_price_with_tax()
3. calculate_tax()

После завершения третьей:

1. create_price_report()
2. calculate_price_with_tax()

После завершения второй:

1. create_price_report()

После завершения первой стек текущих пользовательских вызовов освобождается, и выполнение продолжается в глобальной области.


Что происходит с локальными областями

10. У каждого вызова функции своя локальная область

Рассмотрим:

def calculate_tax(price):
    tax = price * 0.18

    return tax


def calculate_total(price):
    tax = calculate_tax(price)
    total = price + tax

    return total

Во время выполнения:

calculate_total(1000)

у calculate_total() есть собственная локальная область:

price
tax
total

Во время работы:

calculate_tax()

создаётся другая локальная область:

price
tax

Одинаковые имена:

price
tax

не означают, что это одни и те же локальные переменные.

Каждый вызов функции имеет собственную локальную область.

Связь между функциями происходит через:

  • аргументы;
  • возвращаемые объекты;
  • доступные внешние имена.

Функция может обработать результат другой функции

11. Возвращённое значение не обязательно сразу возвращать дальше

Например:

def calculate_discount(price):
    return price * 0.1


def calculate_final_price(price):
    discount = calculate_discount(price)

    discounted_price = price - discount
    service_fee = 100

    return discounted_price + service_fee

calculate_discount() возвращает результат:

discount

Но calculate_final_price() после этого продолжает собственную бизнес-логику:

discounted_price = price - discount
service_fee = 100

и только затем возвращает окончательный результат.

То есть результат одной функции может стать промежуточным значением для другой.


Зачем разделять программу на несколько функций

12. Одна функция — одна понятная задача

Вместо одной большой функции:

def process_order(price):
    ...

можно разделить обязанности:

def calculate_discount(price):
    ...


def calculate_delivery(price):
    ...


def calculate_total(price):
    ...


def create_order_report(price):
    ...

Каждая функция решает отдельную небольшую задачу.

После этого функции можно связывать между собой.

Такой код обычно:

  • проще читать;
  • проще проверять;
  • проще изменять;
  • проще повторно использовать;
  • проще тестировать;
  • проще искать ошибку в конкретной части логики.

Главное по теме

Функция может вызвать другую функцию:

def first_function():
    result = second_function()

Вторая функция может вызвать третью:

def second_function():
    result = third_function()

Когда происходит новый вызов, текущая функция временно ждёт его завершения.

После return Python возвращается именно в то место, откуда функция была вызвана:

Global
↓
Function 1
↓
Function 2
↓
Function 3

После этого возвращение идёт в обратном порядке:

Function 3
↑
Function 2
↑
Function 1
↑
Global

Этот механизм связан со стеком вызовов — Call Stack.

У каждого вызова функции существует собственная локальная область видимости.

Функции обычно передают данные друг другу через аргументы:

calculate_tax(price)

а результаты — через:

return

Благодаря этому большую бизнес-логику можно разбивать на небольшие связанные функции, каждая из которых отвечает за свою часть программы.