如何跳出多个循环?

723

给定以下代码(无法正常工作):

while True:
    # Snip: print out current state
    while True:
        ok = get_input("Is this ok? (y/n)")
        if ok.lower() == "y": break 2 # This doesn't work :(
        if ok.lower() == "n": break

    # Do more processing with menus and stuff

有没有办法让这个工作?还是我必须进行一次检查以跳出输入循环,然后在外部循环中进行另一个更有限的检查,以便在用户满意时一起退出?

193
为什么Python不能只有一个'break(n)',其中n是你想要跳出的层数? - Nathan
14
如果你嵌套了很多循环,使用C++的goto语句会非常方便。 - Drake Johnson
2
@Nathan 看看这个问题:为什么Python不原生支持像goto这样的关键字来跳出n个循环,Nathan给出了一个非常好的解释。 - Shivam Jha
3
Ned Batchelder有一个有趣的演讲,解决了“如何打破两个循环”的问题。剧透警告:将双重循环变为单一循环。 - Tiago Martins Peres
将循环放在try except块中,并通过引发异常来跳出循环。在我看来,这是最易读的方法。 - undefined
39个回答

728

我的第一反应是将嵌套循环重构为一个函数,并使用return来跳出。


3
这是另一个想法,因为get_input_yn()函数在其他地方也会很有用,我相信。 - Matthew Scharley
174
在这种特定情况下同意,但在“我有嵌套循环,我该怎么办”的一般情况下,重构可能没有意义。 - quick_dry
7
通常情况下,可以将内部循环重构为自己的方法,该方法返回 true 表示继续外部循环,返回 false 表示中断外部循环。代码如下:while condition1: / if not MyLoop2(params): break. 另一种方法是设置一个布尔标志,在两个级别上都进行测试。代码如下:more = True / while condition1 and more: / while condition2 and more: / if stopCondition: more = False / break / ... - ToolmakerSteve
8
我同意努力使用 return 是正确的方法,原因是根据Python之禅所说,“扁平优于嵌套”。我们这里有三个层级的嵌套,如果这开始阻碍了代码的可读性,那就是减少嵌套或者至少将整个嵌套提取到一个自己的函数中的时候了。 - Lutz Prechelt
7
我知道这可能很明显,但使用原始代码的示例将改善此答案。 - otocan
显示剩余10条评论

486

这里有另一种简短的方法。缺点是你只能打破外层循环,但有时正是你想要的。

for a in xrange(10):
    for b in xrange(20):
        if something(a, b):
            # Break the inner loop...
            break
    else:
        # Continue if the inner loop wasn't broken.
        continue
    # Inner loop was broken, break the outer.
    break

这里使用了“for / else”结构,具体解释请看:为什么Python在循环中使用“else”关键字?

关键见解:外层循环似乎总是跳出。但如果内层循环没有跳出,那么外层循环也不会跳出。

continue语句才是其中的魔法所在,它在“for-else”子句中。根据定义,如果没有内部跳出,那么就会发生这种情况。在这种情况下,continue巧妙地规避了外部跳出。


20
这很巧妙 :-) ,但不太直接。坦白地说,我不相信保留在Python中使用标记的break或break(n)的论点。绕过它们会增加更多复杂性。 - rfportilla
这非常有效和高效。解决了我的问题,没有任何瑕疵! - Owen Valentinus
2
这种情况下是行不通的。 如果在内部循环中有两个中断,一个旨在仅中断内部循环,而另一个旨在中断两个循环。 - Abdelmonem Mahmoud Amer
所以现在我的代码中有一个链接指向这篇文章,愿stackoverflow永不消失 :) - user7660047
显示剩余5条评论

182

PEP 3136 建议使用带标签的break/continue。Guido 拒绝了这个建议,因为“需要这种特性的代码非常罕见且复杂”。PEP提到了一些解决方法,如异常技巧,而Guido则认为在大多数情况下,重构以使用return会更简单。


106
尽管通常情况下重构/return是更好的选择,但我看到过很多情况下一个简单而精炼的‘break 2’语句会更加合适。此外,对于continue,重构/return并不能达到同样的效果。在这些情况下,数值型的breakcontinue比重构成一个小函数、引发异常或者使用涉及设置嵌套层级标志的复杂逻辑来进行中断更容易理解且不会让代码变得混乱。很遗憾,Guido拒绝了它。 - James Haigh
14
“break; break”会很好。 - PyRulez
11
问题在于你不需要有三个或更多的嵌套循环才会出现问题。两个嵌套循环是相当常见的。 - Jon
19
“Code so complicated to require this feature is very rare” 可以翻译为“需要这个功能的代码非常复杂”。但是,如果您确实使用了如此复杂的代码,则缺少标记循环会使它变得更加复杂,因为您必须手动将break传递到所有循环中。 很愚蠢。 - BallpointBen
4
显然,我只能在5分钟内编辑一篇文章(已经超过6分钟了)。因此,这是我编辑后的帖子:我的看法是:Perl使用标记“last”(称之为'break')和“next”来直接进入下一个循环。这并不罕见-我一直在使用它。我刚开始学习Python,已经有了需要使用它的情况。此外,对于重构来说,使用编号标记会很糟糕-最好是给你想要跳出的循环加上标签,然后使用“break <label>”来明确指定你要跳出哪个循环。 - John Deighan
显示剩余5条评论

159

首先,普通逻辑是有帮助的。

如果由于某种原因无法确定终止条件,异常是备用计划。

class GetOutOfLoop( Exception ):
    pass

try:
    done= False
    while not done:
        isok= False
        while not (done or isok):
            ok = get_input("Is this ok? (y/n)")
            if ok in ("y", "Y") or ok in ("n", "N") : 
                done= True # probably better
                raise GetOutOfLoop
        # other stuff
except GetOutOfLoop:
    pass

针对这个具体的例子,可能并不需要异常处理。

另一方面,在字符模式应用程序中,通常会有“是”、“否”和“查询(Q)”选项。对于“查询”选项,我们希望立即退出。这更具异常性。


11
严肃地说,异常非常便宜且Python语言习惯于使用大量异常。定义和抛出自定义异常也非常容易。 - Gregg Lind
22
有趣的想法。我对是否喜欢它感到矛盾。 - Craig McQueen
12
如果这个解决方案能够将两种方法(1)使用标志变量(done)和(2)引发异常,分别展示出来,那会更有帮助。将它们合并成一个单一的解决方案只会让它看起来更加复杂。对于未来的读者:要么使用所有涉及 done 的行,要么定义 GetOutOfLoop(Exception) 并引发/捕获它。 - ToolmakerSteve
4
通常情况下,除了异常处理之外,使用try块处理其他事情是非常不被赞同的。try块是专门为错误处理而设计的,将它们用于某些奇怪的控制流程在风格上并不好。 - nobillygreen
4
@tommy.carstensen 这是无稽之谈。在Python 2和Python 3中,定义一个新的异常子类并引发它(如答案所示),以及向Exception构造函数传递自定义消息(例如raise Exception('bla bla bla'))都是有效的。在这种情况下,前者更可取,因为我们不希望except块捕获所有异常,而只是我们用于退出循环的特殊异常。如果按照您建议的方法操作,然后我们代码中的一个错误导致引发了意外异常,它将被错误地视为与有意退出循环相同。 - Mark Amery
显示剩余5条评论

72

引入一个新变量,将其用作“循环打破器”。首先将某些值赋给它(False、0等),然后在外部循环中,在退出循环之前,将该值更改为其他值(True、1等)。一旦循环退出,使“父”循环检查该值。以下是示例:

breaker = False #our mighty loop exiter!
while True:
    while True:
        if conditionMet:
            #insert code here...
            breaker = True 
            break
    if breaker: # the interesting part!
        break   # <--- !

如果你遇到无限循环,这是唯一的出路;对于其他循环,执行速度确实快得多。如果有许多嵌套的循环,也可以使用此方法退出全部或部分循环。无止境的可能性!希望这可以帮助你!


1
在我看来,这是最简单和最易读的解决方案。谢谢分享! - sampleuser
4
虽然这是最容易应用的方法,但当你有超过两个需要退出的循环时,它会变得冗长。 - Camilo Martinez M.
这是被要求的[虽然有些hacky]解决方案。谢谢。 - Cfomodz

65

我倾向于认同重构为函数通常是处理这种情况的最佳方法,但如果你真的需要跳出嵌套循环,则以下是 @S.Lott 描述的异常抛出方法的一个有趣变体。它使用 Python 的 with 语句使异常抛出看起来更好一些。定义一个新的上下文管理器(您只需执行一次):

from contextlib import contextmanager

@contextmanager
def nested_break():
    class NestedBreakException(Exception):
        pass
    try:
        yield NestedBreakException
    except NestedBreakException:
        pass

现在您可以按照以下方式使用此上下文管理器:

with nested_break() as mylabel:
    while True:
        print("current state")
        while True:
            ok = input("Is this ok? (y/n)")
            if ok == "y" or ok == "Y": raise mylabel
            if ok == "n" or ok == "N": break
        print("more processing")

优点:(1)它更加简洁(没有显式的try-except块),(2)每次使用nested_break都会得到一个定制的Exception子类;无需每次声明自己的Exception子类。

47

首先,您可能还需要考虑将获取和验证输入的过程封装成一个函数。在函数内部,如果输入正确,则可以直接返回该值;如果不正确,则可以在while循环中继续执行。这实际上消除了您解决的问题,并且通常适用于更一般的情况(跳出多个循环)。如果您绝对必须保留此结构在您的代码中,并且确实不想处理布尔变量...

您也可以使用goto以以下方式(使用此处的愚人节模块):

#import the stuff
from goto import goto, label

while True:
    #snip: print out current state
    while True:
        ok = get_input("Is this ok? (y/n)")
        if ok == "y" or ok == "Y": goto .breakall
        if ok == "n" or ok == "N": break
    #do more processing with menus and stuff
label .breakall

我知道,我知道,“不应该使用goto”之类的话,但在像这样的奇怪情况下它确实很有效。


1
如果它像INTERCAL中的COME FROM命令一样,那么什么也不会发生。 - 1800 INFORMATION
3
我喜欢这个笑话,但堆栈溢出的目的是推广良好的代码,所以我必须投反对票 :( - Christian Oudard
18
我认为这是一个干净且易读的解决方案,足以被视为良好的代码,因此我投赞成票。 :) - J.T. Hurley
1
@J.T.Hurley 不,这并不干净和可读。我的意思是,在任何现实场景中,goto会造成一堆麻烦(虽然在这个例子中可能看起来很干净和可读)。 (而且这也非常违反Python编码风格...) - Alois Mahdal
5
我认为goto命令遭受了不好的评价,任何专业的编程人员都应该能够正确地处理它。 - Albert Renshaw
显示剩余9条评论

45

要打破多个嵌套循环,而不重构到函数中,可以利用内置的StopIteration异常来实现“模拟goto语句”:

try:
    for outer in range(100):
        for inner in range(100):
            if break_early():
                raise StopIteration

except StopIteration: pass

详见此讨论关于使用goto语句来退出嵌套循环。


5
使用这种方法处理异常比创建自己的类要好看得多,而且非常简洁。我是否应该这样做? - mgjk
2
实际上,StopIteration 是用于生成器的,但我认为通常你不会有任何未捕获的 StopIteration 异常。因此,看起来这是一个不错的解决方案,但无论如何创建新异常都不是错误。 - Ekrem Dinçel
1
最好、最简单的解决方案对我来说是: - Alexandre Huat
1
为什么这个答案排在列表的最后?当你迭代并且想要停止迭代时,这是Python中最惯用的方式。对于来自其他编程语言的人来说,它看起来很可怕,但这就是Python的做法! - stevecu

23
keeplooping = True
while keeplooping:
    # Do stuff
    while keeplooping:
          # Do some other stuff
          if finisheddoingstuff():
              keeplooping = False

或类似的东西。

你可以在内循环中设置一个变量,在内循环退出后立即在外循环中进行检查,如果需要则跳出。我有点喜欢使用GOTO方法,前提是你不介意使用愚人节玩笑模块 - 它不像Pythonic,但确实是有道理的。


这是一种标志设置! - SIslam

13

虽然这并不是最美观的方法,但在我看来,它是最好的方法。

def loop():
    while True:
    #snip: print out current state
        while True:
            ok = get_input("Is this ok? (y/n)")
            if ok == "y" or ok == "Y": return
            if ok == "n" or ok == "N": break
        #do more processing with menus and stuff

我相信你也可以使用递归来解决这个问题,但我不确定这是否对你来说是一个好的选择。


这对我来说是正确的解决方案。我的用例与原帖的非常不同。我需要循环遍历基本相同的数据两次来查找排列,因此我不想将两个while循环分开。 - Brian Peterson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接