为什么我的if/else语句在Python函数中递归时,尽管while循环已标记停止,但循环仍然执行两次?
我有一段使用递归的代码:
def rec():
n1=None
run=True
while run:
if n1 == None:
n1=int(input('first number'))
n2=int(input('second number'))
out=n1+n2
i=input(f'{out}enter r f or e')
if i=='e':
run=False
elif i=='r':
n1=out
elif i=='f':
rec()
rec()
这段代码的目的是计算一些数字的总和,可能会用到之前计算的结果作为下一个输入。在每次输入数字后,程序会提示用户输入 e
来退出程序,输入 r
来把上一次的结果作为下一个总和的输入,或者输入 f
来重新开始(这时会要求输入两个新的数字)。我想用递归来实现 f
的逻辑。
我测试这段代码时,第一次选择 f
,第二次选择 e
。结果我发现程序在第三次又让我输入。
这是为什么呢?
1 个回答
关于递归,很多人有一个误解,认为从一个调用返回时,会把整个递归调用链都解开,然后把控制权交回给最初的调用者。其实并不是这样——你不能在返回的过程中跳过任何调用。返回一个递归调用只会把控制权(也就是从调用栈中弹出一次)交给它直接调用的那个函数,然后这个函数会从它暂停的地方继续执行。
另外,每个函数调用都有自己的一套局部变量(状态),其他调用无法读取或修改这些变量。你可以通过参数、返回值和全局、非局部以及类变量在函数之间共享数据,但在你的程序中,这些都没有用到(这其实是好事,尽量让数据保持局部是个好习惯)。
在这两方面,递归函数的行为和非递归函数没有什么不同。
了解了这些背景后,我们来看看你的顶层调用。当你(作为用户)输入f
来执行这个代码块时:
elif i=='f':
rec()
调用的函数会暂停执行,然后递归地运行rec()
。这个子调用和父调用一样,开始时有一套全新的变量。在新的子rec()
调用中,while
循环开始运行,你输入第二组数字。接着,你输入e
来通过把run
设为False
来打破循环。控制权到达子调用的末尾,然后返回给调用者。
当顶层的rec()
调用恢复时,while
循环的下一次迭代会执行,因为在那个函数中run
仍然是True
。记住,run
是一个完全局部的变量,只在每个调用中有效,所以子调用(现在已经不存在了)把run
设为False
对父调用没有影响。
通过上面的例子可以看出,你当前的设计要求用户在每次输入f
时都要输入一个e
。换句话说,每个e
只结束当前的递归调用,而不是所有的递归调用。
避免这个问题最简单的方法就是在每个子调用后无条件地返回:
if i=='e':
# the user wants to exit; just return right
# away instead of flipping a boolean flag
return
elif i=='r':
n1=out
elif i=='f':
# recurse, but when we get back to this call, end
# it immediately instead of testing the loop again
return rec()
少一个布尔标志意味着可修改的状态更少,这样程序更容易理解。即使这里没有错误,去掉run
也是一个有用的简化。
话虽如此,递归在这个用例中根本不合适。它让人困惑,不符合Python的习惯,如果用户尝试进行超过大约1000次的递归调用,程序最终会崩溃。
一般来说,递归只适用于分治场景,这种情况下调用栈是对数增长,而不是线性增长。(如果现在听不懂也没关系——你可以完全避免使用递归,直到你上算法课,这样就会明白了)。
既然你已经有了一个循环,我建议用它来管理整个操作。可以这样做:
def interactively_add_numbers():
n1 = None
while True:
if n1 is None:
n1 = int(input("first number: "))
n2 = int(input("second number: "))
result = n1 + n2
response = input(
f"result = {result}\n"
" 'r' (rerun with result as n1)\n"
" 'f' (start fresh)\n 'e' (exit)\n> "
)
if response == "e":
return
elif response == "r":
n1 = result
elif response == "f":
n1 = None
interactively_add_numbers()
作为练习,我建议处理输入错误和类型错误。如果用户在需要数字的地方输入了字母,程序会不太优雅地崩溃。如果用户输入了'e'
、'r'
、'f'
以外的选项,程序会在n1
不变的情况下重新运行,这会让人感到意外。
顺便说一下,始终使用Black格式化你的代码,这样其他程序员才能更容易阅读。