为什么我的if/else语句在Python函数中递归时,尽管while循环已标记停止,但循环仍然执行两次?

3 投票
1 回答
69 浏览
提问于 2025-04-13 14:23

我有一段使用递归的代码:

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 个回答

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格式化你的代码,这样其他程序员才能更容易阅读。

撰写回答