【问题标题】:A few things about decorator functions关于装饰器函数的一些事情
【发布时间】:2021-02-26 17:09:33
【问题描述】:

我试图理解一个使用装饰器存储已计算数字值的斐波那契数列示例。比如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. 为什么在wrap() 中使用*args?它不应该只取一个数字 n 并检查它的值是否在字典中吗?为什么args 在某些地方用* 调用,而在某些地方没有*
  2. 当函数fib 被递归调用时会发生什么(装饰器函数的行为方式)。我首先认为它在每次递归期间都会进入该函数,但这是不对的,因为值字典会重置?那么它是否只输入了wrap() 函数?
  3. 为什么最后会返回 wrap?

【问题讨论】:

    标签: python recursion decorator python-decorators functools


    【解决方案1】:

    1- 你完全正确。不需要“*”,因为您只检查传递给函数的值。所以简单地称它为“n”。

    2- 首先让我们弄清楚在您使用“@dec”之后的标签“fib”是什么?实际上,它现在是装饰器内部的内部功能(我的意思是“包装”功能)。为什么 ?因为@dec 实际上是这样做的:

    fib = dec(fib)
    

    所以“dec”装饰器被调用,它返回什么? “包装”功能。什么是“包装”功能?这是一个包含“值”字典的闭包。

    每当你调用你的装饰器时,装饰器的主体只会执行一次。所以只有一个“值”字典。在执行“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

      查看闭包以了解有关其工作原理的更多信息。

      为什么装饰器最后返回wrap

      装饰器基本上是用另一个函数替换一个函数。它像函数一样在@ 之后调用变量,然后用该调用的结果替换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 之间的区别归结为 tuple unpacking。您可以从上面的输出中看到 args 包含,例如(1, )*args 将其解压缩为 1

        因此,通过在包装函数中使用args,它实际上并不是将数字作为键存储在您可能怀疑的values 字典中,而是包含数字的元组。在这种情况下,可以进行解包,但这是一个不必要的额外步骤。

        这也使您可以很好地了解递归的发生方式,例如在第一次调用 fib(n) 时,返回语句的第一部分是 return fib(n - 1),因此需要在 fib(n-2) 之前对其进行评估,所以这会立即导致从 n1 的每个值的记忆,然后返回递归堆栈,评估 fib(n-2),但所有这些结果都可以对values 感到满意,无需进一步递归调用fib

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-04-19
          • 2021-06-22
          • 2014-03-29
          • 1970-01-01
          • 2021-04-06
          • 1970-01-01
          • 1970-01-01
          • 2019-05-26
          相关资源
          最近更新 更多