0:00
[МУЗЫКА]
[МУЗЫКА] Сейчас
мы поговорим о механизме запуска функций в языке Python и о рекурсии.
Рекурсией называется вызов функцией самой себя.
В этом нет ничего страшного, мы уже пробовали вызывать из функции другие
функции, и ничего не происходило страшного.
Чтобы лучше понять, что такое рекурсия, нужно разделить два понятия,
то есть функция у нас теперь — это не просто функция,
а две вещи: это собственно команды, текст программы,
и информация о значении локальных переменных,
параметров и точке, где сейчас исполняется код,
то есть на каком месте в этой функции мы остановили выполнение.
Совокупность этой информации назовем экземпляром функции.
При рекурсивном запуске, когда функция вызывает саму себя,
команды выполняются одни и те же.
Те, что записаны у нас в коде этой функции.
Но при этом экземпляры функции различны, то есть у каждой функции свои
параметры и свое место, где сейчас должно продолжиться
выполнение этой функции, после того как отработает следующий вызванный экземпляр.
При этом рекурсивная функция предназначена для решения каких-то задач,
где можно часть работы поручить другому.
То есть общий подход такой: вы, во-первых, проверяете,
не пора ли заканчивать действие в рекурсивной функции,
затем выполняете какие-то действия, которые должен сделать этот экземпляр,
и поручаете все остальные действия другим экземплярам функции.
Посмотрим на примере.
Итак, у нас есть задача развернуть последовательность.
Последовательность уже традиционно для нас задается как набор чисел,
оканчивающихся нулем.
Ноль — это признак конца последовательности.
Требуется вывести ее задом наперед.
При этом мы не знаем заранее, сколько там будет чисел в этой последовательности,
не можем завести для них переменные, и пока что не знаем никаких способов,
как можно сохранить последовательность большой длины.
В приведенной реализации можно заметить рекурсию.
Она позволяет как раз решить эту задачу, не создавая каких-то списков,
массивов или чего-нибудь другого.
Как устроена наша рекурсивная функция?
Во-первых, каждый экземпляр функции не получает никаких параметров.
Данные он будет получать от пользователя, считывать их с помощью input.
Собственно, первым делом он и сохраняет это в свою
локальную переменную n, то есть считывается очередное число.
Признаком того, что пора заканчивать работу, выступает if, которое проверяет,
что число не равно нулю.
И какие-то действия делаются только в том случае,
если последовательность еще не закончилась.
Как вообще решается эта задача?
Мы считали число, запомнили его, остались еще какие-то числа,
но это уже не наша проблема.
Вызвав рекурсивную функцию, мы поручаем эту проблему следующим экземплярам.
То есть в принципе можно представить себе каких-то людей, сидящих рядом.
И например, преподаватель идет вдоль ряда и называет каждому число.
Каждый записывает на листочке одно свое число и преподавателю
говорит: а теперь идите к следующему.
Это как раз рекурсивный запуск функции.
Если же преподаватель назвал число 0 какому-то очередному сидящему в ряду
студенту, то этот студент говорит: я не буду делать ничего.
И отодвигает преподавателя обратно к предыдущему студенту.
То есть функция заканчивает свою работу и возвращается в тот экземпляр,
который ее вызвал.
Собственно, тот, к кому преподаватель подходил в прошлый раз,
например, сидящему слева от нас.
И когда возвращается исполнение, наш преподаватель,
то он возвращается ровно к той строке, на которой закончилось выполнение программы.
То есть можно представить, что студенту напечатана вот такая инструкция,
что ему делать, и когда он доходит до состояния вызов функции reverse следующей
закончился, он делает следующее действие: собственно называет записанное число.
Если преподаватель будет записывать названные числа себе на листочек, в свою
очередь, то у него получится как раз развернутая исходная последовательность.
Посмотрим подробнее, как это все устроено,
как все это сохраняется в памяти.
Вот код нашей функции, та же самая.
Что происходит?
Сначала у нас есть вызов функции из какой-то основной программы —
откуда-то мы должны рекурсию нашу вызвать.
В таблице это экземпляр № 1.
У него есть локальная переменная, которую оно считывает, единственная n.
У каждого экземпляра функции она своя.
Мы запоминаем число введенное — пусть это будет троечка, и запоминаем,
куда мы должны вернуться, когда функция закончит свою работу.
Никаких проблем у нас раньше с этим не было.
Мы вызывали, например, из своей основной программы функцию.
Куда-то у нас управление проваливалось у нее, что-то там делалось,
и в конце концов мы возвращались в то место, откуда функцию вызвали.
Экземпляр функции помнит то место, куда нужно вернуться,
после того как он закончит свою работу.
Первый экземпляр функции, в свою очередь, еще не закончит последовательность
рекурсивных вызовов, а вызовет второй экземпляр, который, в свою очередь,
будет хранить свою локальную переменную и место, куда нужно вернуться.
Я обозначил это названием функции, в которую она должна вернуться,
номером экземпляра этой функции и строкой,
фактически где должно продолжиться выполнение.
Если посчитать, как раз получится нужный номер строки.
Вообще говоря, это не строка.
Почему?
Потому что в одной строке у вас может быть несколько вызовов разных функций,
и нужно помнить не только номер, но и конкретное, условное место в этой строке.
Но здесь у нас всего один вызов, поэтому ничего страшного в этом нет.
На самом деле номер экземпляра функции, куда нужно возвращаться,
запоминать вовсе необязательно.
Почему? Потому что у нас каждый раз активна только
одна функция, стоящая в конца стека — последняя вызванная,
и куда возвращаться, довольно очевидно — в предыдущую функцию.
Здесь у нас представлены все четыре вызова.
Последняя функция уже ничего печатать не будет,
она просто вернется: считает и вернется в предыдущий третий экземпляр.
Примерно такую картину вы увидите в отладчике,
когда будете отлаживать свои рекурсивные функции,
там тоже можно посмотреть весь стек вызовов — так это называется.
Стек — это значит, что мы кладем только наверх, как стопка книг, например,
такая, и снимать можем столько сверху, из середины выдернуть не можем.
Такая абстракция очень хорошо описывает как раз процесс запуска функции,
у нас всегда активный экземпляр лежит сверху, и когда мы его заканчиваем,
убираем ровно одну штуку и попадаем в то место, где мы должны продолжать работу.
Многие боятся рекурсии, но на самом деле ничего страшного в ней нет, нужно
только осмыслить происходящий процесс и потренироваться писать программы.
Мы для вас подготовили много довольно несложных задач, которые, конечно же,
можно было гораздо проще решить без рекурсии, но мы поставили некоторые
ограничения, и воспользоваться циклами, например, в этих задачах вы не можете.
Придется решать их рекурсией.
На самом деле это только поможет вам, потому что задачи несложные, а там,
где действительно нужна рекурсия, где она действительно облегчает решение задачи,
появляются уже достаточно сложные в реализации.
Поэтому сначала потренируйтесь на простых,
а затем уже можно будет перейти к сложным рекурсивным решениям.
А чтобы было проще делать это, я продемонстрирую вам процесс
отладки рекурсивных функций и еще несколько примеров решения задач.
[МУЗЫКА]
[МУЗЫКА]