Зарегистрируйтесь, чтобы продолжить обучение

Итеративный процесс Python: Функции

Рекурсию можно использовать разными способами: тот, который мы рассматривали в предыдущем уроке, называется рекурсивным процессом. Данный урок будет более понятен, если вы уже разобрались с предыдущей темой и выполнили упражнение.

Другой способ использования рекурсии в коде называется итеративным процессом. Название запутывает: и рекурсивный и итеративный процесс — оба описывают рекурсию.

Помните наборы вызовов из предыдущего урока? Каждый новый созданный экземпляр, или ящик функции factorial(), ожидает от следующего экземпляра, что тот сделает возврат какого-нибудь значения. Никакого вычисления не производится, пока мы не спустимся до конца, к базовому случаю. Только тогда мы получим 1 и начнем выполнять умножения в обратном порядке: 1 умноженная на 2 — это 2, затем 3 умножается на два, получается 6.

С факториалом 3 никаких проблем, но представьте, что нужен факториал 100. Программе потребуется хранить в памяти множество чисел из-за откладывания всех операций умножения. Откладывание здесь — ключевое слово: суть рекурсивного процесса в откладывании вычислений до самого конца. Ничего не будет умножаться, пока процесс не спустится к базовому случаю, а если его остановить, программа ничего не будет знать и вы не получите никакой полезной информации, так как не дадите ей полностью закончить задачу. Рекурсивный процесс похож на человека, который выполняет работу за всю неделю в пятницу после обеда.

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

Вот код.

def factorial(n):
    if n == 0:
        return 1

    def inner(counter, acc):
        if counter == 1:
            return acc
        return inner(counter - 1, counter * acc)

    return inner(n, 1)

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

Функция факториала содержит в себе другую функцию. Помните, определения функций — это не сами ящики, а всего лишь их описания. Внутренняя функция inner() принимает два аргумента: counter и accumulator. counter отслеживает движение от n до 1. А accumulator — текущий результат умножения чисел от n до 1. Если counter достигает 1, accumulator возвращается — в этот момент он будет равен конечному ответу.

После того как функция определена, у нас остается единственная строка в функции факториала: вернуть результат вызова функции inner() с n и 1 в качестве аргументов.

Давайте запустим factorial(3). Единственная строка, которая на самом деле запускается в функции factorial() это return inner(n, 1). Она хочет вернуть и завершиться, но ей нужно получить значение от функции inner().

Создается ящик inner(). Он принимает 3 и 1. Внутри ящика inner(), 3 известно как counter, а 1 как acc. Значение counter не равно 1, поэтому первое условие не выполняется. Затем он хочет сделать возврат, но ему необходимо получить значение от нового экземпляра inner(). Это самая важная часть: новый ящик вызывается с counter уменьшенным на 1, потому что мы прошли один шаг, а accumulator был умножен на counter.

Создается новый ящик inner(). Он принимает 2 и 3 — counter и accumulator. counter не равен 1, поэтому он пытается вернуть значение, но не может — ему нужно получить значение от нового экземпляра inner(), вызванного с 2 - 1 в качестве первого аргумента, и 2 * 3 в качестве второго. Мы еще не закончили, но выполнили полезное умножение — 2 * 3 это одно из умножений, необходимых для нахождения 3!

Создается новый inner() ящик. Он принимает 1 и 6. Теперь первое условие удовлетворено, поэтому этот ящик просто возвращает accumulator, число 6.

Затем значение 6 просачивается в первый inner() ящик, затем в ящик факториал, а затем возвращается в виде ответа.

Так выглядят вычисления шаг за шагом:

inner(3, 1)  # inner(3 - 1, 3 * 1)
inner(2, 3)  # inner(2 - 1, 2 * 3)
inner(1, 6)  # counter == 1, return 6
6

В любой момент программе необходимо помнить состояние, но его размер всегда неизменный — всего два числа.

Подобный итеративный процесс в целом может быть описан так:

  1. Определить начальное состояние. В нашем случае мы делаем первый вызов inner() с n и 1. Это начальное состояние
  2. Проверить терминальный сценарий. Мы проверяем, не равен ли counter числу 1 и останавливаем рекурсию, когда он принимает значение 1
  3. Определить новое состояние. Это то, как процесс двигается вперед. В нашем случае мы делаем еще один вызов inner() с уменьшенным counter и умноженным accumulator. Два этих новых числа определяют новое состояние
  4. Повторить шаг 2

И эта штука повторяется, пока не доберется до терминального сценария.

Давайте повторим вкратце.

  1. Рекурсия — это когда что-то содержит себя в своем описании
  2. Рекурсивный процесс — это процесс вычисления с отложенными вычислениями
  3. Итеративный процесс — это процесс вычисления, когда состояние может быть описано фиксированным количеством значений

Обратите внимание, что условие в функции inner() не включает ветвь else. Поэтому, если условие (counter == 1) не удовлетворяется, происходит переход к следующей строке и выполняется return inner(counter - 1, counter * acc).

def inner(counter, acc):
    if counter == 1:
        return acc
    return inner(counter - 1, counter * acc)

Это работает, потому что когда инструкция return исполнена, экземпляр функции выплевывает значение и исчезает. Больше ничего не будет происходить после того, как выполнится return.

Поэтому, когда условие удовлетворяется, происходит выполнение return acc, а второй возврат уже не происходит.

Вот еще пример:

def some_function(a):
    return 100
    return a
    return 4123123

Эта функция будет всегда возвращать 100. Две других возвращаемых инструкции никогда не выполнятся, потому что экземпляр функции уничтожается после исполнения первого возврата. Только один возврат может производиться в любом конкретном экземпляре функции.

Резюме

Вызовем функцию из предыдущего урока:

factorial(3)           # ничего не происходит, вычисляем множители
3 * factorial(2)       # ничего не происходит, вычисляем множители
3 * 2 * factorial(1)   # ничего не происходит, вычисляем множители
3 * 2 * 1              # наконец начинаем умножение
3 * 2
6

Процесс вычисления, который создает эта функция, называется рекурсивным процессом. Основная его идея — откладывание вычисления до самого конца.

Вся информация о вычислениях, обо всем, что запоминает программа в любой конкретный момент (вычислениях, константах, функциях), называется состоянием. Множество проблем в программировании рождается из необходимости справиться с состоянием.

Суть итеративного процесса — вычисление с фиксированным количеством состояний.

def factorial(n):
    if n == 0:
        return 1

    def inner(counter, acc):
        if counter == 1:
            return acc
        return inner(counter - 1, counter * acc)

    return inner(n, 1)

Идея:

  1. Считаем от n до 1
  2. Берем число из предыдущего шага
  3. Умножаем это число на текущее число
  4. Передаем его в новый шаг
  5. Когда counter достигает 1, число передается из предыдущего шага в ответ

Нам нужно с чего-то начать, потому что шаг 2 требует число из предыдущего шага, и мы начинаем с 1, потому что тогда n * 1 будет просто n:

return inner(n, 1)

Вот так выглядят вызовы inner(), когда происходит вычисление 3!:

inner(3, 1)   # inner(3 - 1, 3 * 1)
inner(2, 3)   # inner(2 - 1, 2 * 3)
inner(1, 6)   # counter == 1, return 6
6

Дополнительные материалы

  1. Рекурсия, рекурсивный процесс и итеративный процесс
  2. Визуализация итеративного процесса

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
Программирование на Python, Разработка веб-приложений и сервисов используя Django, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»