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



