Подпрограммы: процедуры и функции, параметры и область видимости

Подпрограммы: процедуры и функции, параметры и область видимости Полезное

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

Ниже разберем формальные и фактические параметры, передачу по значению и по ссылке (и почему в Python это третий вариант), область видимости и рекурсию. Примеры проверены на Python 3.14 и C (стандарт C11).

Мини-словарь

Термин Что значит Пример
Подпрограмма Общее название именованного блока кода функция, процедура, метод
Функция Возвращает значение, вызов можно подставить в выражение s = area(3, 4)
Процедура Выполняет действие без возвращаемого значения procedure PrintLine в Pascal, void в C
Метод Подпрограмма, привязанная к классу или объекту items.append(1)
Формальный параметр Имя в заголовке подпрограммы width в def area(width, height)
Фактический параметр (аргумент) Значение или выражение в месте вызова 3 в area(3, 4)

Граница деления «функция vs процедура» зависит от языка. В Pascal это разные ключевые слова (function и procedure). В C есть только функции, а процедура — это функция с типом void. В Python любая функция что-то возвращает: если return нет, результатом будет None.

Минимальный пример: функция и процедура

def area(width, height):
    """Функция: вычисляет и возвращает значение."""
    return width * height


def print_report(name, value):
    """Процедура по смыслу: только печатает, return нет."""
    print(f"{name}: {value}")


s = area(3, 4)
print_report("Площадь", s)
print_report("Сумма площадей", area(3, 4) + area(2, 5))

result = print_report("Проверка", 1)
print(result)

В консоли появится:

Площадь: 12
Сумма площадей: 22
Проверка: 1
None

Разбор. Заголовок def area(width, height) задает имя и формальные параметры. Вызов area(3, 4) подставляет фактические параметры и превращается в число 12, поэтому его можно складывать с другим вызовом. print_report полезна своим побочным эффектом (выводом), а ее «результат» — None, и присваивать его обычно бессмысленно.

Зачем нужны подпрограммы

Главная причина — не экономия памяти, как иногда пишут, а управление сложностью:

  • Повторное использование. Код пишется один раз, ошибка исправляется в одном месте.
  • Декомпозиция. Большая задача разбивается на подзадачи с понятными именами: load_data, validate, save_report.
  • Тестирование. Функцию с явными входами и выходом легко проверить отдельно.
  • Абстракция. Вызывающему коду достаточно знать, что делает подпрограмма, а не как.

Выделять подпрограмму оправдано и для однократного кода, если это цельная подзадача.

Как происходит вызов

При вызове программа запоминает точку возврата, передает аргументы, выполняет тело и возвращается к следующей инструкции после вызова. В компилируемых языках это обычно реализовано через стек вызовов: для каждого вызова создается кадр (frame) с параметрами, локальными переменными и адресом возврата. Код сохранения и восстановления контекста называют прологом и эпилогом.

Это упрощенная модель: компилятор может встроить функцию (inline) или передать аргументы через регистры.

Способы передачи параметров

Способ Что получает подпрограмма Видно ли изменение снаружи Где встречается
По значению Копию значения Нет C (по умолчанию), Pascal без var
По ссылке Ссылку на саму переменную Да, и присваивание, и изменение Pascal var, C++ T&, C# ref
Передача ссылки на объект (call by sharing) Ссылку на тот же объект, но не на переменную вызывающего Изменение объекта — да, новое присваивание имени — нет Python, Java для объектов, JavaScript
По имени Выражение, которое вычисляется при каждом обращении Зависит от выражения Algol 60, историческая схема

В Pascal есть еще параметры-константы (const): изменять их внутри подпрограммы запрещено, а способ передачи (копия или ссылка) выбирает компилятор.

По значению и по ссылке на примере C

В C все аргументы передаются по значению. Чтобы функция изменила переменную вызывающего, ей передают адрес (указатель) — это эмуляция передачи по ссылке.

#include <stdio.h>

void swap_by_value(int a, int b) {
    int t = a; a = b; b = t;       /* меняются только копии */
}

void swap_by_pointer(int *a, int *b) {
    int t = *a; *a = *b; *b = t;   /* меняются исходные переменные */
}

int main(void) {
    int x = 1, y = 2;
    swap_by_value(x, y);
    printf("by value:   x=%d y=%d\n", x, y);
    swap_by_pointer(&x, &y);
    printf("by pointer: x=%d y=%d\n", x, y);
    return 0;
}

Вывод:

by value:   x=1 y=2
by pointer: x=2 y=1

В Pascal тот же эффект дает слово var в заголовке:

procedure Swap(var a, b: Integer);
var t: Integer;
begin
  t := a; a := b; b := t;
end;

Python: не по значению и не по ссылке

В Python параметр получает ссылку на тот же объект, что и аргумент, но это не ссылка на переменную вызывающего кода. Поэтому работают два разных правила: изменение изменяемого объекта (список, словарь) видно снаружи, а присваивание параметру нового значения — нет.

def rebind(items):
    items = [0]          # имя items теперь указывает на новый список


def mutate(items):
    items.append(99)     # меняется сам объект, общий с вызывающим


def add_one(n):
    n = n + 1            # int неизменяем: создается новый объект
    return n


data = [1, 2]
rebind(data)
print(data)

mutate(data)
print(data)

k = 10
add_one(k)
print(k)
print(add_one(k))

Вывод:

[1, 2]
[1, 2, 99]
10
11

Практическое правило: если функция должна «вернуть изменения» неизменяемого значения (число, строку, кортеж), возвращайте результат через return и присваивайте его. Если функция меняет переданный список, отразите это в имени и документации.

Ловушка: изменяемое значение по умолчанию

Значение по умолчанию вычисляется один раз при определении функции, а не при каждом вызове.

def add_item_bad(item, bucket=[]):
    bucket.append(item)
    return bucket


print(add_item_bad("a"))
print(add_item_bad("b"))


def add_item(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket


print(add_item("a"))
print(add_item("b"))

Вывод:

['a']
['a', 'b']
['a']
['b']

Неверный вариант копит элементы между вызовами, исправленный создает новый список.

Область видимости и глобальные переменные

Локальная переменная существует только внутри вызова подпрограммы. Глобальная объявлена на уровне модуля и видна всем функциям. В Python присваивание имени внутри функции делает его локальным для всей функции, отсюда частая ошибка:

counter = 0


def inc_bad():
    counter += 1


try:
    inc_bad()
except UnboundLocalError as e:
    print("Ошибка:", type(e).__name__)


def inc(value):
    return value + 1


counter = inc(counter)
print(counter)

Вывод:

Ошибка: UnboundLocalError
1

Исправить можно словом global counter, но надежнее передать значение параметром и вернуть результат, как в inc. Функция, которая зависит только от своих аргументов, предсказуема и тестируется без подготовки глобального состояния.

Вложенные подпрограммы (функция внутри функции) видны только внутри внешней. В Python для изменения переменной внешней функции нужно слово nonlocal.

Рекурсия кратко

Подпрограмма может вызывать саму себя. Обязательны базовый случай (где рекурсия останавливается) и шаг, приближающий к нему.

import sys


def factorial(n):
    if n < 0:
        raise ValueError("n должно быть >= 0")
    if n <= 1:           # базовый случай
        return 1
    return n * factorial(n - 1)


print(factorial(5))
print(sys.getrecursionlimit())

Вывод:

120
1000

Каждый рекурсивный вызов занимает кадр стека. В CPython глубина по умолчанию ограничена (обычно 1000, значение можно изменить через sys.setrecursionlimit), при превышении возникает RecursionError. Для глубоких повторений в Python обычно выбирают цикл.

Если не получилось

  • Функция «ничего не вернула», в переменной None. Забыт return или он стоит внутри условия, которое не выполнилось.
  • Переменная снаружи не изменилась. В Python присваивание параметру не меняет аргумент: верните значение и присвойте его.
  • Список изменился неожиданно. Функция мутирует переданный объект; передайте копию list(data) или создавайте новый список внутри.
  • UnboundLocalError. Имя присваивается внутри функции и читается до присваивания; передайте значение параметром.
  • RecursionError. Нет базового случая или глубина слишком велика; проверьте условие остановки или перепишите циклом.

Выводы

  • Подпрограмма — именованный блок кода с параметрами; функция возвращает значение, процедура выполняет действие, а граница между ними зависит от языка.
  • Формальные параметры — имена в заголовке, фактические — значения в месте вызова.
  • По значению передается копия, по ссылке — сама переменная; Python передает ссылку на объект (call by sharing), поэтому мутация видна снаружи, а присваивание — нет.
  • Локальные переменные и явный return делают код предсказуемее, чем изменение глобальных.
  • Рекурсии нужен базовый случай; в CPython глубина ограничена.

Где применяется / связь с практикой

Освойте тему на практике

Подпрограммы — основа любого кода: от скрипта обработки файлов до веб-сервиса, где каждый обработчик запроса — функция. Умение правильно выделять функции, выбирать параметры и избегать побочных эффектов отличает поддерживаемый код от одноразового. Системно отработать функции, области видимости и структуру программы на задачах можно на курсе «Python Developer. Basic». Разобраться с отдельными темами на живых примерах помогут открытые уроки Otus.

FAQ

Сколько параметров допустимо у функции?
Жесткого предела обычно нет, но больше 3-4 параметров затрудняет чтение; помогают именованные аргументы или объект-параметр.

Можно ли вернуть из функции несколько значений?
В Python да: return a, b возвращает кортеж, который распаковывают x, y = f(). В C для этого используют структуру или указатели-параметры.

Чем подпрограмма отличается от макроса?
Макрос (#define в C) подставляется текстом до компиляции, без вызова и проверки типов; подпрограмма вызывается во время выполнения.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)