Несколько вещей о функциях декоратора

#python #recursion #decorator #python-decorators #functools

#python #рекурсия #декоратор #python-декораторы #функциональные средства

Вопрос:

Я пытаюсь понять пример последовательности Фибоначчи, используя декоратор для хранения значений уже вычисленных чисел. Например fib(5) , будет вычислен, и когда мы доберемся до fib(6) этого, он не будет вычисляться fib(5) снова… Я немного разбираюсь в декораторах, но некоторые вещи меня просто смущают. У меня есть несколько вопросов о приведенном ниже коде.

 from functools import wraps
def dec(func):
    values = {}
    @wraps(func)
    def wrap(*args):
        if args not in values:
            values[args] = func(*args)
        return values[args]
    return wrap

@dec
def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1)   fib(n - 2)
  
  1. Почему *args используются в wrap() ? Разве не предполагается просто взять число n и проверить, есть ли его значение в словаре? Почему args в некоторых местах вызываются с, а в некоторых без * ?
  2. Что происходит, когда функция fib вызывается рекурсивно (как ведет себя функция декоратора). Сначала я подумал, что он вводит эту функцию во время каждой рекурсии, но это не может быть правильно, потому что словарь значений будет сброшен? Итак, затем он вводит только wrap() функцию?
  3. Почему он возвращает wrap в конце?

Ответ №1:

1- Вы совершенно правы. Нет необходимости в «*», так как вы проверяете только то значение, которое передается функции. Так что просто назовите это «n».

2- Сначала давайте выясним, что такое метка «fib» после того, как вы использовали «@dec» поверх нее? На самом деле теперь это ваша внутренняя функция внутри вашего декоратора (я имею в виду функцию «обертывания»). почему ? потому что @dec на самом деле делает это:

 fib = dec(fib)
  

Итак, вызывается декоратор «dec», что он возвращает? функция «wrap». Что такое функция «wrap»? Это замыкание, которое имеет этот словарь «значений».

Всякий раз, когда вы вызываете свой декоратор, тело декоратора выполняется только один раз. Итак, существует только один словарь «значений». Что еще происходит во время выполнения тела декоратора «dec»? Ничего, кроме возврата ссылки на функцию «wrap». вот и все.

Теперь, когда вы вызываете свою функцию «fib» (первоначально функцию «wrap»), это закрытие выполняется нормально, поскольку это просто рекурсивная функция, за исключением того, что она обладает дополнительной функцией кэширования.

3- Потому что вам нужно иметь дескриптор внутренней функции (здесь функция «wrap»). Вы хотите вызвать его позже, чтобы вычислить Фибоначчи.

Ответ №2:

Что такое *args

*args сопоставляет все оставшиеся аргументы в виде кортежа. Возьмем этот пример:

 def f(*args):
    print(args)
f('a', 'b')
# output: ('a', 'b')
  

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

Что происходит, когда fib вызывается рекурсивно

При оформлении функции с @ помощью, ссылка немедленно перезаписывается. При fib вызовах fib() внутри себя он сначала ищет переменную с этим именем в локальной области видимости. Поскольку их нет, они будут отображаться в следующей области, которая в данном случае является глобальной областью. Там он находит переменную с именем fib , которая фактически назначается wrap функции из вашего декоратора, с «контекстом» исходного fib существа func .

Посмотрите closures для получения дополнительной информации о том, как это работает.

Почему декоратор возвращается wrap в конце

Декоратор в основном заменяет одну функцию другой. Он вызывает переменную после функции @ like a , а затем заменяет новую функцию, определенную def с помощью, результатом этого вызова. В этом случае вы хотите заменить его wrap новой функцией, которая может вызывать или не вызывать старую функцию.

Если вы ничего не возвращаете, переменной fib просто будет присвоено значение None (возвращаемое значение по умолчанию), и вы не сможете вызвать None .

Ответ №3:

Вы можете получить хорошее представление о том, что здесь происходит, просто добавив несколько операторов печати, например:

 from functools import wraps
def dec(func):
    values = {}
    @wraps(func)
    def wrap(*args):
        print("args: ", args, " *args:", *args, args not in values, values)
        if args not in values:
            values[args] = func(*args)
        return values[args]
    print("Wrap", wrap)
    return wrap

@dec
def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1)   fib(n - 2)

print("Answer", fib(5))
  

Итак, результат этого:

 Wrap <function fib at 0x7facac4b70d0>
args:  (5,)  *args: 5 True {}
args:  (4,)  *args: 4 True {}
args:  (3,)  *args: 3 True {}
args:  (2,)  *args: 2 True {}
args:  (1,)  *args: 1 True {(2,): 1}
args:  (2,)  *args: 2 False {(2,): 1, (1,): 1, (3,): 2}
args:  (3,)  *args: 3 False {(2,): 1, (1,): 1, (3,): 2, (4,): 3}
Answer 5
  

Чтобы начать с последней части вашего вопроса, как вы можете видеть из выходных данных, функция завершается в начале программы, когда Python выполняет весь код до того, как он перейдет к print оператору. Это происходит только один раз, и именно это позволяет вашему последующему вызову fib вызывать уже обернутую функцию.

Для того, чтобы функция-оболочка работала, ей необходимо знать, какие аргументы передаются обернутой функции, и она делает это, *args чтобы увидеть переданный параметр, который ей нужен для запоминания его и его результата.

Разница между args и *args сводится к распаковке кортежей. Вы можете видеть из приведенного выше вывода, который args содержит, например (1, ) , и *args распаковывает это в just 1 .

Итак, используя args in обернутую функцию, она фактически сохраняет числа не как ключи в values словаре, как вы могли бы подозревать, а кортеж, содержащий число. В этом случае можно было бы выполнить распаковку, но это был бы ненужный дополнительный шаг.

Это также дает вам хорошее представление о том, как происходит рекурсия, как при первом вызове fib(n) , первая часть оператора return return fib(n - 1) , так что это должно быть оценено перед fib(n-2) , так что это немедленно приводит к запоминанию каждого значения от n до 1, а затем вы возвращаете обратностек рекурсии, оценка fib(n-2) , но все эти результаты могут быть values удовлетворены без дальнейших рекурсивных вызовов fib .