Рекурсия
В программировании рекурсия — вызов функции из неё же самой, непосредственно или через другие функции.
В Python, как и в других языках программирования, есть возможность вызывать функции рекурсивно.
Для примера, попробуем вычислить произведение всех элементов списка рекурсивно:
1 2 3 4 | def multiply_all(array): if not array: return 1 return array[0] * multiply_all(array[1:]) |
И примеры использования:
>>> print(multiply_all([10])) 10 >>> print(multiply_all([1, 2, 3, 4])) 24 >>> print(multiply_all([0, 2, 3])) 0
Что делает данный код?
- Определение функции. Если вдруг кто не знает, что это такое, читаем курс Основы Python с нуля, статью про функции в Python
- Если массив пустой
- Возвращаем 1 (произведение 0 элементов считаем равным единице)
- Иначе возвращаем результат выражения
Разберём это выражение подробнее. array[0] * multiply_all(array[1:]) — это произведение первого элемента массива (array[0]) на multiply_all(array[1:]). multiply_all(array[1:]), в свою очередь, результат той же самой функции multiply_all, но применённый к массиву без первого элемента.
Другими словами, из функции мы рекурсивно вызываем её же саму (но с другим аргументом). То есть, функция multiply_all - рекурсивная.
Условие остановки
В любой рекурсивной функции необходимо условие остановки. Это та часть, когда рекурсивного вызова не происходит. В нашем случае, это вызов с пустым массивом.
Если убрать условие остановки, то рекурсивные вызовы будут выполняться бесконечно.
def example(array): return example(array) print(example([10]))
Traceback (most recent call last): File "", line 6, in <module> print(multiply_all([10])) ^^^^^^^^^^^^^^^^^^ File "", line 3, in multiply_all return multiply_all(array) ^^^^^^^^^^^^^^^^^^^ File "", line 3, in multiply_all return multiply_all(array) ^^^^^^^^^^^^^^^^^^^ File "", line 3, in multiply_all return multiply_all(array) ^^^^^^^^^^^^^^^^^^^ [Previous line repeated 996 more times] RecursionError: maximum recursion depth exceeded
На самом деле, как мы видим, Python не позволяет бесконечно вызывать рекурсивную функцию. В Python есть счётчик глубины, при превышении которого происходит исключение. В разных реализациях Python это значение разное, в Python 3.12 это, например, 1000.
Более того, это означает, что нашу функцию multiply_all нельзя корректно вызвать для массивов длиной более 1000.
Узнать и изменить это значение можно с помощью sys.getrecursionlimit и sys.setrecursionlimit.
>>> import sys >>> print(sys.getrecursionlimit()) 1000 >>> sys.setrecursionlimit(2000) >>> print(sys.getrecursionlimit()) 2000
На самом деле, проще и производительнее будет НЕ использовать рекурсию, и написать multiply_all через цикл:
def multiply_all(array): result = 1 for element in array: result *= element return result
Для чего полезна рекурсия
Для рекурсивных определений. Рекурсивное определение — определение какого-либо объекта внутри самого этого объект, то есть ситуация, когда объект является частью самого себя. Рассмотрим хрестоматийный пример.
Числа Фибоначчи — это последовательность чисел, которые задаются по определённому правилу. Оно звучит так: каждое следующее число равно сумме двух предыдущих. Первые два числа заданы сразу и равны 0 и 1.
Раз определение рекурсивное, то можно написать и рекурсивную функцию для его вычисления. Функция, по номеру в последовательности Фибоначчи возвращающая элемент:
def fibonacci(n): if n == 0: return 0 if n == 1: return 1 return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(10)) # 55
Производительность рекурсии
Рекурсивные вызовы выполняются крайне медленно. Это связано с тем, что для каждого вызова функции необходимо подготовить свою область видимости, передать аргументы, и ещё запомнить все возвращаемые значения в правильном порядке.
Даже в примере с числами Фибоначчи можно заметить, что код уже для fibonacci(40) выполняется крайне долго.
Это связано не только с фактом наличия рекурсии, а с тем, что мы выполняем рекурсивный вызов дважды каждый раз. Из-за этого количество вызовов растёт экспоненциально.
На самом деле, вычисления чисел Фибоначчи также можно переписать через цикл:
def fibonacci(n): if n == 0: return 0 if n == 1: return 1 fibonacci1 = 0 fibonacci2 = 1 for i in range(2, n+1): fibonacci1, fibonacci2 = fibonacci2, fibonacci1 + fibonacci2 return fibonacci2
Однако не каждое рекурсивное определение будет легко поддаваться переделке в цикл; иногда проще оставить рекурсивную реализацию, и не вызывать для значений, занимающих много времени.
Сложная рекурсия
Возможна чуть более сложная схема: функция A вызывает функцию B, а та в свою очередь вызывает A. Это называется сложной рекурсией.
Сложно привести разумный пример; просто знайте, что такое тоже существует.
Если кто-то не до конца понял рекурсию, есть отличная статья про неё.