Python 函数追踪
为了让递归的过程更清晰,这里有一个例子:点击这里查看。
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 个回答
如果你只是把缩进级别存储在 indent
里,那它只会在当前的函数调用中有效。每次调用这个函数时,都会得到一个新的变量,初始值是0。把它存储在函数对象里,就能确保每次调用这个函数时,它的值都是一样的(在Python中,函数也是对象)。
至于第二部分,我不太明白你在问什么。每当参数大于1时,都会调用两次fib函数,因此不会返回任何值。只有当参数等于1或0时,才会进行返回。
如果我们直接使用
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
第二个
print()
调用在至少一次调用f()
返回之前是不会被执行的。因为它在代码中出现在调用f()
之后,所以执行流程只有在f()
返回后才会到达这里。
哇,终于开始了!
首先,你有一个函数,任何函数。在你的例子中,就是 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个比我先回答的人没有让这个问题变得过时。
干杯!
(*) 是的,是的,鸭子类型等等可调用对象之类的,没什么关系。