Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 14 из 20 Начальный 15 мин 100 XP

Рекурсия в Python: функция вызывает себя

Функция, которая вызывает сама себя, звучит как ловушка — но с базой и шагом это инструмент для задач, повторяющих самих себя.

Редакция Питоники

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

Обратный отсчёт: база и шаг

отсчёт вызовами себя
def countdown(n):
    if n == 0:
        print('Старт!')
        return
    print(n)
    countdown(n - 1)

countdown(3)
Вывод
3
2
1
Старт!

У каждой рекурсии две части: базовый случай, который останавливает вызовы, и шаг, приближающий задачу к базе. В примере базой служит условие n == 0: дойдя до нуля, функция печатает «Старт!» и выходит через return — никаких новых вызовов. Шаг — строка countdown(n - 1): каждый следующий вызов получает число на единицу меньше и потому гарантированно когда-нибудь упрётся в базу. Вызовы выстраиваются цепочкой: countdown(3) зовёт countdown(2), тот — countdown(1), тот — countdown(0), где цепочка и обрывается.

Что будет без базы

ошибка: рекурсия без остановки
# Базы нет — остановки тоже нет
def endless(n):
    print(n)
    endless(n - 1)

endless(5)
Функция вызовет себя без конца, и Python остановит цепочку ошибкой RecursionError: maximum recursion depth exceeded. Лимит глубины по умолчанию — около тысячи вызовов, узнать его можно функцией sys.getrecursionlimit().

Факториал: рекурсивное решение

Классика рекурсии — факториал: произведение всех чисел от единицы до n. Определение само рекурсивно: n факториал — это n, умноженное на (n - 1) факториал, а единица факториал равен единице. Формула почти дословно переписывается в код:

факториал вызовами себя
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))
Вывод
120

База — n <= 1: факториал единицы и нуля равен единице, дальше дробить нечего. Шаг — return n * factorial(n - 1): ответ для пяти живёт внутри ответа для четырёх. Цепочка раскрутится вниз до базы, а затем ответы начнут подниматься обратно: единица, дважды два, трижды шесть, четыре раза двадцать четыре, пять раз сто двадцать. Обратите внимание: return стоит на каждой ветке — рекурсивная ветка обязана вернуть своё значение наверх, иначе результат не доедет до первого вызова:

ошибка: потерянный return
# Так не работает: рекурсивная ветка ничего не возвращает
def broken(n):
    if n <= 1:
        return 1
    n * broken(n - 1)

print(broken(3))
Вывод будет None: ветка рекурсии вычислила n * broken(n - 1), но не вернула результат, — а при больших n умножение на None падает с TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'. Результат рекурсии всегда возвращают через return.

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

таблица факториалов
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

for i in [1, 2, 3, 4, 5, 6]:
    print(i, factorial(i))
Вывод
1 1
2 2
3 6
4 24
5 120
6 720

Ещё шаг: сумма цифр числа

Рекурсия удобна везде, где задача распадается на маленькую часть и ту же задачу для остатка. Разбирая число на цифры, мы откусываем последнюю цифру — n % 10 — и повторяем то же для оставшегося n // 10:

сумма цифр вызовами себя
def digit_sum(n):
    if n < 10:
        return n
    return n % 10 + digit_sum(n // 10)

print(digit_sum(2947))
Вывод
22

База здесь — однозначное число: её сумма равна ей самой. Дальше цепочка: семь плюс сумма 294, четыре плюс сумма 29, девять плюс сумма 2 — и двойка упирается в базу. В сумме двадцать два. Ни один вызов не знает всей картины — каждый откусывает по цифре и доверяет остальное следующему вызову себя же.

Рекурсия по данным: вложенные списки

Самое родное применение рекурсии — данные, вложенные сами в себя. Список может содержать списки, а те — свои списки, и заранее неизвестно, сколько уровней вложенности принесёт реальная задача. Цикл не знает числа вложений; рекурсия просто повторяет себя для каждого встреченного списка:

разглаживание вложенного списка
def flatten(items):
    out = []
    for item in items:
        if isinstance(item, list):
            out.extend(flatten(item))
        else:
            out.append(item)
    return out

data = [1, [2, [3, 4], 5], 6]
print(flatten(data))
Вывод
[1, 2, 3, 4, 5, 6]

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

Рекурсия или цикл

РекурсияЦикл
Данные вложены сами в себяДанные лежат одной лентой
Задача распадается на меньшую такую жеПовторы идут по списку или счётчику
Каждый вызов держит свои переменные в памятиОдна ячейка аккумулятора
Факториал, обход дерева, разглаживаниеСуммы, счётчики, печать списков

Факториал, кстати, ложится на обе лошади: цикл с аккумулятором решает его без единого вызова себя — и делает это дешевле по памяти:

тот же факториал циклом
acc = 1
for i in [1, 2, 3, 4, 5]:
    acc *= i
print(acc)
Вывод
120
  • разглаживание вложенных списков и словарей — глубина известна только данным, не коду;
  • обход каталогов файловой системы: папки содержат папки, и заранее неизвестно, насколько глубоко;
  • деревья: разделы документа, комментарии с ответами, структуры вроде DOM;
  • математические определения, которые сами рекурсивны: факториал, числа Фибоначчи, сумма цифр.

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

Дерево: структура, созданная для рекурсии

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

подсчёт комментариев в дереве
def count_comments(node):
    total = 1
    for reply in node['replies']:
        total += count_comments(reply)
    return total

tree = {
    'text': 'корень',
    'replies': [
        {'text': 'ответ 1', 'replies': []},
        {'text': 'ответ 2', 'replies': [
            {'text': 'ответ 2.1', 'replies': []}
        ]}
    ]
}

print(count_comments(tree))
Вывод
4

Функция считает узел и прибавляет подсчёт каждого потомка. Потомок — это такой же словарь с полями text и replies, поэтому функция вызывает себя без всякой проверки типа: структура гарантирует, что рекурсия закончится на листьях, где replies пуст и цикл не сделает ни шага. Четыре узла — корень и три ответа — четыре и получилось.

Так же считают файлы в каталоге (внутри папки бывают папки), разделы документа, комментарии с ответами, элементы JSON неизвестной вложенности. Общее у всех этих задач одно: структура повторяет саму себя, и код, который это повторение отражает, читается почти так же коротко, как сама структура.

Чек-лист из четырёх пунктов

Что проверитьКак это выглядитКак выглядит ошибка
База естьif n <= 1: return 1забыли условие вообще
Шаг приближает к базесравните n и n - 1countdown(n + 1)
Ветка возвращает значениеreturn перед рекурсивным вызовомn * f(n - 1) без return
Глубина разумнаменьше сотни вызововфакториал тысячи

Четыре строчки чек-листа — и рекурсия перестаёт быть лотереей. Пункт первый ловит вечную цепочку, второй — цепочку, ушедшую не туда, третий — потерянный результат, четвёртый — переполнение глубины.

ошибка: база недостижима
# Так не запускается: база на единице, а стартуем с нуля
def total(n):
    if n == 1:
        return 1
    return n + total(n - 1)

total(0)
Python ответит RecursionError: maximum recursion depth exceeded. Начали с нуля, условие n == 1 недостижимо, а шаг уводит счётчик в минус: 0, -1, -2 — база никогда не сработает. Правильное условие — n <= 1.

Глубину рекурсии можно поднять командой sys.setrecursionlimit(...), но мы этого не советуем: лимит стоит там не просто так, а глубокая рекурсия съедает память стека, и на реальных данных вместо красивого RecursionError вы получите падение всего процесса. Если задача упирается в глубину — скорее всего, её надо решать циклом.

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

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

def f(n):
    if n == 0:
        return 0
    return n + f(n - 1)

print(f(4))
def countdown(n):
    if n == 0:
        return 'старт'
    return countdown(n - 1)

print(countdown(3))
def strange(n):
    if n <= 0:
        return 1
    return strange(n - 2)

print(strange(7))
Проверь себя
0 / 9

1. Что такое базовый случай рекурсии?

2. Что произойдёт при вызове рекурсивной функции без базового случая?

3. Как рекурсивный factorial(3) приходит к ответу 6?

4. Зачем return в рекурсивной ветке, например return n * factorial(n - 1)?

5. В каком случае цикл лучше рекурсии?

6. Почему flatten для вложенных списков удобнее написать рекурсией?

7. Функция total(n) объявлена как: if n == 1: return 1, иначе return n + total(n - 1). Что будет при вызове total(0)?

8. Что вернёт count_comments для дерева, где у корня один ответ, а у того ответа — ещё один?

9. Почему рекурсия обычно медленнее цикла?

Карточки терминов
Запомнено: 0 / 8
Практика

Напиши рекурсивную функцию power(base, exp), возводящую base в натуральную степень exp (включая ноль): power(2, 5) вернёт 32. База рекурсии — exp равен нулю, результат единица; шаг — base умножить на power(base, exp - 1). Напечатай power(2, 5) и power(3, 3).

practice.py
Вопросы и ответы по уроку

Что такое рекурсия в Python простыми словами?

Это функция, которая вызывает саму себя. У каждой рекурсии две обязательные части: базовый случай — условие остановки, и шаг — вызов себя с задачей поменьше, приближающей к базе. Например, факториал: factorial(n) возвращает n * factorial(n - 1), а на единице останавливается и возвращает 1.

Урок 14: рекурсия — функция вызывает себя

Что такое RecursionError и как его избежать?

RecursionError: maximum recursion depth exceeded — Python остановил цепочку вызовов на лимите глубины около тысячи. Причины две: забытый базовый случай или шаг, который не приближает задачу к базе, например countdown(n) вместо countdown(n - 1). Как читать такую трассировку — в уроке об ошибках функций.

Урок 19: ошибки функций и трассировка

Как написать факториал рекурсивно и есть ли вариант без рекурсии?

Рекурсивно: def factorial(n): if n <= 1: return 1; return n * factorial(n - 1) — base плюс шаг. Без рекурсии работает цикл с аккумулятором: acc = 1, затем acc *= i по числам от одного до n. Для факториала цикл дешевле по памяти — он не держит стопку вызовов.

Урок: числа в цикле — суммы и факториалы

Когда использовать рекурсию вместо цикла?

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

Почему рекурсия медленнее цикла?

Каждый вложенный вызов создаёт кадр стека с собственным набором локальных переменных — это и время, и память. Цикл держит одну ячейку аккумулятора. На длинных линейных повторах разница заметна, поэтому там мы и выбираем цикл.

Можно ли увеличить лимит глубины рекурсии в Python?

Технически да: sys.setrecursionlimit(5000). Но мы этого не советуем — лимит защищает от переполнения стека, и за его пределами процесс может просто упасть. Если задача упирается в глубину, скорее всего, её стоит переписать циклом.

Как понять, что база рекурсии недостижима?

Проверьте, может ли шаг дойти до условия остановки. Классика — база на n == 1 при старте с нуля: шаг уводит счётчик в минус, условие никогда не срабатывает, и цепочка падает с RecursionError. Безопасная база выглядит как n <= 1.

Как выбрать между рекурсией и циклом?

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

Понравился урок? Сошлитесь на него

«У каждой рекурсии две части: базовый случай, который останавливает вызовы, и шаг, приближающий задачу к базе.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.