Рекурсия предлагает программистам удобный способ разбить более крупные проблемы на управляемые части. Рассмотрим итерационные и рекурсивные решения для суммы Фибоначчи.

# iterative
def fib_iterative(n):
      if (n == 0):
              return 0
      elif (n == 1):
              return 1
      elif (n >1 ):
              fn = 0
              fn1 = 1
              fn2 = 2
              for i in range(3, n):
                      fn = fn1+fn2
                      fn1 = fn2
                      fn2 = fn
              return fn
# recursive
def fib(n):
    if n == 0:return 0
    if n == 1:return 1
    else: return fib(n-1) + fib(n-2)

Рекурсивные решения часто легче читать и писать для проблем ветвления. Обходы по деревьям, графам и математические ряды (часто) обрабатываются более интуитивно с помощью рекурсии.

Хотя это обеспечивает удобство, затраты времени на вычисление рекурсии для задач ветвления растут экспоненциально с увеличением значения n.

Давайте посмотрим на стек вызовов для fib (6)

Мы выполняем примерно вдвое больше операций на каждом последующем уровне дерева. Дает нам временную сложность O (2 ^ n).

Если мы внимательно рассмотрим наше дерево, вы заметите, что мы повторяем работу. fib (2) вычисляется пять раз, fib (3) вычисляется трижды и так далее. Хотя это не проблема для малых значений n, представьте себе количество повторяющейся работы при вычислении fib (1000). После того, как мы пересмотрим наше рекурсивное решение, попробуйте выполнить одну и ту же задачу (скажем, 20) для обеих версий и обратите внимание на значительную разницу во времени выполнения.

Конечно, есть хороший способ предотвратить повторяющуюся работу и сохранить наше элегантное решение?

Воспоминание: вы можете съесть свой торт и съесть его!

С помощью мемоизации мы можем «запоминать» (запоминать, сохранять) результат проблем, с которыми мы сталкивались раньше, и возвращать сохраненный результат вместо повторения вычислений.

Например, после вычисления fib (2) мы можем сохранить результат и использовать его в следующие четыре раза, когда будет встречаться fib (2).

Это сокращает время выполнения с O (2 ^ n) до O (n), потому что мы вычисляем каждое из fib (0)… fib (n) только один раз.

Один из способов добиться этого - использовать словарь.

__fib_cache = {}
def fib_memo(n):
    if n in __fib_cache:
        return __fib_cache[n]
    
    else:
      __fib_cache[n] = n if n < 2 else fib_memo(n-2) + fib_memo(n-1)
        return __fib_cache[n]

И вуаля! Мы успешно запомнили рекурсивную функцию!

Однако мы можем пойти еще дальше и создать абстрактную мемоизацию для любой рекурсивной функции!

Декораторы: функции, использующие функции для выполнения других функций.

Декораторы известны как «функции высшего порядка»: функции, которые принимают другие функции в качестве параметров. Здесь мы будем использовать декоратор для обобщения мемоизации.

Вот оформленное решение:

def memoize(func):
    cache = func.cache = {}
    @functools.wraps(func)
    def memoized_func(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key not in cache:
            cache[key] = func(*args, **kwargs)
        return cache[key]
    return memoized_func
@memoize
def fibonacci(n):
    if n == 0:return 0
    if n == 1:return 1
    else: return fib(n-1) + fib(n-2)

Давайте разберем его построчно.

Наш декоратор - это функция, которая принимает другую функцию в качестве аргумента, мы объявляем ее как таковую в строке 1.

Затем мы объявляем словарь func.cache = {}, который объявлен как свойствоfunc. Здесь мы будем хранить каждый уникальный вызов, сделанный func.

Мы вернемся к @functools.wraps.

def memoized_func(*args, **kwargs): - Внутри декоратора мы определяем новую мемоизированную функцию на основе переданного func, и она принимает те же параметры, что и func. Мы называем эту новую версию memoized_func.

*args - ключевое слово, указывающее, что memoizer принимает произвольное количество аргументов, например, def memoize(one two three) или def memoize(just_one) оба допустимы. Аналогично *kwargs представляет произвольное количество аргументов ключевого слова (параметры, определенные при вызове функции), например. def memoize(one = 1, two = 2, three = 3). Примечание. Вы можете использовать эти специализированные параметры в любой функции.

Следующая строка объявляет key для нашего func.cache на основе заданных параметров. Если мы видели эту комбинацию параметров раньше, мы извлекаем результат из func.cache, в противном случае создаем новый ключ, используя исходный func, как он был определен (например, используя fib), и присваиваем его значение результату, например (сохранить результат fib(2)).

Последняя строка внутри memoized_func() возвращает результат единственного вызова func с заданными параметрами.

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

Наконец, мы помечаем нашу исходную функцию знаком @memoize. Обозначение «@» указывает компилятору передать функцию непосредственно под ним в функцию более высокого порядка с именем memoized_func, которая в нашем случае возвращает мемоизированную версию fibonacci(). Эта версия функции немедленно вернет результаты, которые мы вычислили ранее, в противном случае она вызовет исходную fib() функцию, чтобы определить, каким должно быть это число.

Что такое @ functools.wraps?

@functools.wraps - еще один декоратор, встроенный в Python. Он позволяет decoratormemoize хранить информацию, относящуюся к запомненной строке документации функции или имени функции, чтобы к информации можно было получить доступ позже. Если бы мы спросили у python fib.__name__ или fib.__doc__ (они встроены в свойства функций), без @functools.wraps мы бы вернули имя и строку документации для memoized_fib, а не самого fib. В целях отладки полезно иметь доступ к имени и строке документации исходной вызываемой функции, а не к функции украшения, поскольку один и тот же декоратор может применяться к множеству функций.

Попробуй сам!

В следующий раз, когда вы столкнетесь с рекурсивной функцией, подумайте, можно ли улучшить ее производительность с помощью мемоизации, а затем перенесите эту мемоизацию на другие функции, добавив декоратор!