Python 函数追踪

3 投票
3 回答
2217 浏览
提问于 2025-04-16 15:02

为了让递归的过程更清晰,这里有一个例子:点击这里查看

def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

def trace(f):
    f.indent = 0
    def g(x):
        print('|  ' * f.indent + '|--', f.__name__, x)
        f.indent += 1
        value = f(x)
        print('|  ' * f.indent + '|--', 'return', repr(value))
        f.indent -= 1
        return value
    return g


fib = trace(fib)
print(fib(4))

我能理解“trace”这个函数的作用,但不太明白它是怎么实现的。具体来说:

1) 为什么我们用 f.indent 而不是简单的 indent = 0(我知道这样做不行,但不明白原因)。

2) 我不明白

print('|  ' * f.indent + '|--', 'return', repr(value))

为什么在找到值之前,这部分代码不会被执行。

有没有人能详细解释一下整个过程呢?

3 个回答

1

如果你只是把缩进级别存储在 indent 里,那它只会在当前的函数调用中有效。每次调用这个函数时,都会得到一个新的变量,初始值是0。把它存储在函数对象里,就能确保每次调用这个函数时,它的值都是一样的(在Python中,函数也是对象)。

至于第二部分,我不太明白你在问什么。每当参数大于1时,都会调用两次fib函数,因此不会返回任何值。只有当参数等于1或0时,才会进行返回。

4
  1. 如果我们直接使用 indent 而不是 f.indent,那么 indent 就会变成一个局部变量,这个变量只在内部函数 g() 里有效,因为在 g() 中对 indent 的赋值会影响它。看起来这是在用 Python 3,其实不一定要用函数属性,你也可以使用 nonlocal 这个关键词来处理。

    def trace(f):
        indent = 0
        def g(x):
            nonlocal indent
            print('|  ' * indent + '|--', f.__name__, x)
            indent += 1
            value = f(x)
            print('|  ' * indent + '|--', 'return', repr(value))
            indent -= 1
            return value
        return g
    
  2. 第二个 print() 调用在至少一次调用 f() 返回之前是不会被执行的。因为它在代码中出现在调用 f() 之后,所以执行流程只有在 f() 返回后才会到达这里。

7

哇,终于开始了!

首先,你有一个函数,任何函数。在你的例子中,就是 fib()。在 Python 中,函数也是对象,而且可以在运行时创建,所以我们实际上可以这样做:

def give_me_a_function():
    def f(x):
        return x

    return f

(警告:接下来会频繁出现“函数”这个词)

好吧,我们定义了一个不需要任何参数的函数,并且返回了……另一个函数?没错!函数就是对象!你可以在运行时创建它们!所以我们在原来的函数里面定义了第二个函数,并把它返回,就像返回其他对象一样。

现在,让我们做一些稍微复杂一点的事情:

def alter(other_function):
    def altered(x):
        return other_function(x) + 1

    return altered

这到底是什么?

我们定义了一个函数 alter()。就像上面的例子一样,它在运行时创建一个函数并返回它,作为一个对象。这部分我们已经讲过了。

既然函数是对象,可以被创建和返回,那为什么不能把一个函数作为参数传递呢?而且在调用的时候使用它!没错:alter() 接受一个函数作为参数(*),并使用它。

要实现 alter(),只需要把上面的魔法和这个新魔法结合起来:我们接收一个函数作为参数,动态创建一个使用它的新函数,并返回这个新函数对象!

让我们试试。

>>> def f(x):
...     return 2*x
>>> new_function = alter(f)
>>> f(2)
4
>>> new_function(2)
5

搞定了!alter() 接受我的 f(),创建一个新函数,这个新函数会返回 f() + 1,然后把它作为返回值给我。我把它赋值给 new_function,现在我有了一个新的、自己制作的、在运行时创建的函数。

(我之前警告过你“函数”这个词会用得很频繁,对吧?)

现在,回到你的代码。你做的事情比简单的 f() + 1 更复杂吗?还是没什么区别?其实,你创建了一个新函数,它调用原来的函数,并打印一些数据。这和我们刚才做的没什么神奇的地方。有什么大不同吗?

其实,有一个细节:fib() 是递归的,所以它会调用自己,对吧?不!它不是调用自己。它调用的是 fib(),而你恰好做了这个:

fib = trace(fib)

哇,fib() 不再是它自己了! 现在 fib()trace(fib)!所以当 fib() 进入递归时,它不是在调用自己,而是在调用我们创建的那个包装版本。

这就是为什么缩进是这样处理的。再看看 trace(),现在知道它实际上是在递归缩进,这样就有道理了,对吧?你想要每个递归层级有一个缩进,所以增加它,调用 fib()(记住,现在是 trace(fib)),然后当我们返回时(也就是递归完成并准备返回到调用链的上一步时)我们再减少它。

如果你还是不明白,试着把所有功能都移到 fib() 里面。忘掉装饰函数,那实在是太让人困惑了。

啊,我真希望这能帮到你,也希望那2000个比我先回答的人没有让这个问题变得过时。

干杯!

(*) 是的,是的,鸭子类型等等可调用对象之类的,没什么关系。

撰写回答