Learning Platform
Глоссарий Troubleshooting
Урок 18.05 · 25 мин
Начальный
recursion-limitsetrecursionlimitexplicit-stackiteration-conversionpython-3.13

Лимит 1000 — почему именно столько

Python устанавливает sys.getrecursionlimit() == 1000 по умолчанию. Это число выбрано как компромисс:

  • Достаточно высоко для большинства практических рекурсий (log(n) на огромных данных — около 30-40).
  • Достаточно низко, чтобы предотвратить segfault на стандартном OS-стеке 8 МБ при средних frames CPython.

Лимит проверяется в C-функции _Py_CheckRecursiveCall на каждый CALL_FUNCTION. При превышении бросается RecursionError. Это не настоящий stack overflow ОС, это защита Python ДО реального переполнения.

import sys
print(sys.getrecursionlimit())   # 1000 (по умолчанию)

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

count(990)    # ok
count(1000)   # RecursionError: maximum recursion depth exceeded

Конкретное число «1000 vs 990» зависит от: глубины стека к моменту вызова (если main позвал hands main позвал hands), количества скрытых frames (вызовы внутри генераторов, контекст-менеджеров).

sys.setrecursionlimit — когда безопасно

Поднять лимит можно:

import sys
sys.setrecursionlimit(10_000)

Безопасно ли это? Зависит от того, насколько вы близки к реальному пределу OS-стека.

Расчёт. OS-stack обычно 8 МБ (Linux/macOS, main thread). Один CPython frame ~500 байт. Максимально безопасная глубина: 8 МБ / 500 байт = ~16 000 frames. С запасом 50% — 8000-10000.

sys.setrecursionlimit(10_000)   # вероятно безопасно

sys.setrecursionlimit(1_000_000)   # почти наверняка segfault

С 1_000_000 Python не выдаст RecursionError, потому что искусственный лимит не превышен. Но OS-стек переполнится — процесс умрёт с segmentation fault без шанса обработать исключение.

DANGER

sys.setrecursionlimit(10**6) — антипаттерн. Если у вас глубокая рекурсия, проблема не в лимите Python, а в выборе алгоритма. Переписывайте на итерацию.

Изменение в потоках (threading)

Если рекурсия в Python-thread, OS-стек обычно меньше: 1 МБ по умолчанию. Соответствующий лимит — около 2000 frames.

Поэтому есть классический баг: «работает в main, падает в потоке с тем же кодом». Решение — задать размер стека до создания потока:

import threading

threading.stack_size(8 * 1024 * 1024)   # 8 МБ

def worker():
    deep_recursion(5000)   # теперь ok

t = threading.Thread(target=worker)
t.start()

Заметьте: stack_size() влияет только на потоки, создаваемые после вызова, и только на новые. Main-thread имеет размер, заданный ОС, изменить его сложнее.

Как переписать рекурсию через явный стек

Любая рекурсия эквивалентна циклу с явным стеком. Главная идея: вместо использования call stack ОС, мы держим стек состояний в обычном Python list.

# рекурсивный DFS дерева
def dfs_recursive(node):
    if node is None:
        return
    print(node.value)
    dfs_recursive(node.left)
    dfs_recursive(node.right)

# итеративный с явным стеком
def dfs_iterative(root):
    if root is None:
        return
    stack = [root]
    while stack:
        node = stack.pop()
        print(node.value)
        # push в обратном порядке: правый первый, чтобы левый вышел первым
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)

Разница: рекурсивный код использует call stack ОС, итеративный — Python list в heap. Heap может расти до миллиардов элементов (упирается только в RAM), стек — нет.

Итеративная версия не падает на глубоких деревьях. Это главная причина переписывать рекурсию в DFS, parsing и подобных задачах для production.

Шаблон преобразования

Универсальный шаблон, превращающий рекурсию в итерацию:

def recursive(state):
    if base(state):
        return base_value
    sub = recursive(transform(state))
    return combine(state, sub)

# превращается в
def iterative(initial_state):
    stack = [(initial_state, False)]    # (state, processed_subcall?)
    result_stack = []

    while stack:
        state, processed = stack.pop()
        if base(state):
            result_stack.append(base_value(state))
        elif not processed:
            # сначала надо обработать подзадачу
            stack.append((state, True))                 # потом мы вернёмся к combine
            stack.append((transform(state), False))    # сначала обработать sub
        else:
            # подзадача обработана, её результат сверху result_stack
            sub = result_stack.pop()
            result_stack.append(combine(state, sub))

    return result_stack[0]

Это сложнее, чем рекурсия — потому что мы вручную моделируем то, что компилятор делает автоматически. Но это работает на любой глубине.

На практике, если рекурсия простая (один tail call) — переписывайте в while. Если ветвистая — используйте описанный шаблон.

Tail recursion в Python 3.13

В Python 3.13 (релиз 2024) появилась специфическая оптимизация — computed gotos в интерпретаторе и некоторые tail calls на C-уровне. Это даёт 10-15% общего ускорения для bytecode-интенсивного кода.

Важно: это не TCO для Python-функций. Ваши def f(...): return f(...) всё равно создают новый frame и упираются в setrecursionlimit. Изменения коснулись только того, как C-интерпретатор переключается между bytecode-инструкциями.

Гвидо неоднократно подтверждал: TCO для Python-уровня не планируется. Если ваш алгоритм требует tail-рекурсию глубиной миллион — пишите цикл.

Замеры — что реально безопасно

Эксперимент: какая максимальная глубина рекурсии работает на вашей машине?

import sys

def find_max_depth():
    sys.setrecursionlimit(100_000)   # высокий лимит Python

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

    lo, hi = 1, 100_000
    while lo < hi:
        mid = (lo + hi + 1) // 2
        try:
            deep(mid)
            lo = mid
        except RecursionError:
            hi = mid - 1

    return lo

# можно упасть segfault'ом! раскомментируйте на свой риск
# print(f"max safe depth: {find_max_depth()}")

На моей машине (macOS, Python 3.13, 8 МБ stack) — около 14 000-20 000 frames до RecursionError. Если поднять лимит ещё выше — попадёшь в реальный OS stack overflow, segfault.

Что делать при глубокой рекурсии

Решения по приоритету: проблему рекурсии решить, не лимит увеличивать.

1 (лучше)переписать в циклwhile или for с явным стеком — работает на любой глубине
2ограничить глубинуесли рекурсия от пользовательских данных — ограничьте ввод
3поднять лимит до 10000безопасно для большинства machines, но не для всех (Windows 1 МБ stack)
4 (плохо)поднять до миллионапуть к segfault, без шанса на обработку

Реальные сценарии «нужна глубокая рекурсия»

  1. Парсинг глубоко вложенного JSON. Файл с массивом массивов массивов на 5000 уровней. JSON-парсер на рекурсии падает. Решение — итеративный парсер (как в json модуле — он на C, без проблем).

  2. DFS большого графа. Дерево или граф с цепочками длиной 10000+. Решение — переписать на итеративный DFS с явным стеком.

  3. Регулярные выражения с глубоким backtracking. Pathological regex может уйти в глубокий backtracking и падать. Решение — переписать regex (атомарные группы, possessive quantifiers).

  4. Парсер выражений (calculator). Скобки на 5000 уровней. Решение — Pratt parser или Shunting-yard алгоритм, итеративные оба.

В каждом случае правильное решение — алгоритмическое, а не «увеличить setrecursionlimit».

Попробуй сам

Дано бинарное дерево. Сделайте рекурсивный sum_tree (сумма всех узлов) и итеративный с явным стеком. Сравните, что работает на дереве с цепочкой длиной 5000.

import sys
sys.setrecursionlimit(2000)

class Node:
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

# построим вырожденное дерево-цепочку
root = Node(1)
cur = root
for i in range(2, 5001):
    cur.left = Node(i)
    cur = cur.left

# рекурсивный — упадёт
def sum_recursive(node):
    if node is None:
        return 0
    return node.val + sum_recursive(node.left) + sum_recursive(node.right)

# итеративный — работает
def sum_iterative(root):
    if root is None:
        return 0
    total = 0
    stack = [root]
    while stack:
        node = stack.pop()
        total += node.val
        if node.left:
            stack.append(node.left)
        if node.right:
            stack.append(node.right)
    return total

try:
    print(sum_recursive(root))
except RecursionError:
    print("recursive failed: too deep")

print(sum_iterative(root))   # 12502500, ok
Потоки и процессы: размер стека на поток и изоляция памяти
Проверка знанийKnowledge check
У вас глубокий парсинг JSON с массивами 5000 уровней вложенности. RecursionError. Какие три варианта решения по убыванию правильности и почему 'просто увеличить setrecursionlimit до миллиона' — плохой выбор?
ОтветAnswer
1) Лучший вариант — использовать стандартный модуль json: он написан на C и не использует Python call stack для рекурсии, работает на любой глубине. 2) Хороший вариант — переписать свой парсер с явным стеком в Python list: state-machine с push/pop состояний, работает на любой глубине, упирается только в heap RAM. 3) Допустимый вариант — поднять setrecursionlimit до 8000-10000: безопасно при стандартном OS-стеке 8 МБ, главное не превышать ~16000 frames. Почему 'миллион' плохо: setrecursionlimit — это ИСКУССТВЕННЫЙ лимит Python, ставится ДО реального переполнения OS-стека (обычно 8 МБ). При лимите 10^6 Python не остановит вас на 100 000 frame, но OS-stack кончится — процесс умрёт segfault'ом без возможности обработать. SIGSEGV ловить нельзя, программа просто прекращается. В проде это катастрофа.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. По умолчанию sys.getrecursionlimit() равен 1000. Почему именно это число?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5