如何理解Python循环中的`else`子句?

192

许多Python程序员可能不知道 while 循环和 for 循环的语法中包含一个可选的 else:子句:

for val in iterable:
    do_something(val)
else:
    clean_up()
else子句中的语句是一些特定清理操作的好地方,并且在循环正常终止时执行:也就是说,用returnbreak退出循环会跳过else子句; 在continue之后退出执行它。我之所以知道这一点,仅仅因为我刚刚(再次)查了一下文档,因为我总是记不住何时执行else子句。

总是吗?在循环“失败”时,正如名称所示?在正常终止时?即使循环使用return退出?我永远无法确定,除非查询文档。

我把持续的不确定归咎于关键字的选择:对于这种语义,我发现else极难记忆。我的问题不是“为什么要使用此关键字来实现此目的”(尽管在阅读答案和评论后,我可能会投反对票),而是如何考虑else关键字使其语义具有意义,从而可以记住它?

我相信这方面有很多讨论,我可以想象选择是为了与try语句的else:子句保持一致(我也必须查阅它),并且不将其添加到Python保留字列表中。也许选择else的原因会澄清它的功能并使它更易记,但我关注将名称与功能连接起来,而不仅仅是历史解释。

这个问题的答案曾短暂地被关闭为重复,其中包含许多有趣的背景故事。我的问题有不同的焦点(如何将else的具体语义与关键字选择联系起来),但我觉得应该在某个地方链接到这个问题。


6
@alexis Dharman的编辑是为了使您的问题不基于个人意见,同时保持问题本身,我认为这次编辑使帖子好很多(且不值得关闭)。 - Nick stands with Ukraine
@Dharman,我很感激你的努力,但是你的编辑完全扭曲了问题的意图和内容。请停止。 - alexis
3
如何理解else关键字的语义,以便记住它?请注意,具体解释如何帮助您个人记住else的工作方式并不是一个有用的问题,因为如果没有这个问题,它会被认为是基于个人意见的。 - Nick stands with Ukraine
1
感谢您的解释,@nick。然而,Dharman的标题使它成为一个非常不同的问题的重复。如果您确信这太基于个人观点,我可以接受您的投票。但请不要动这个问题。 - alexis
1
还有一个问题是关于理解这个设计,而不是它的作用是什么。 - alexis
15个回答

218

if语句会在其条件评估为false时运行else子句。同样,while循环会在其条件评估为false时运行else子句。

这个规则与您所描述的行为相匹配:

  • 在正常执行中,while循环重复运行直到条件评估为false,因此自然退出循环将运行else子句。
  • 当您执行break语句时,您会在不评估条件的情况下退出循环,因此条件无法评估为false,您永远不会运行else子句。
  • 当您执行continue 语句时,您会再次评估条件,并且完全像在循环迭代开始时一样执行。所以,如果条件为真,则继续循环,但如果条件为假,则运行else子句。
  • 其他退出循环的方法,如return,不会评估条件,因此不运行else子句。

for循环的行为方式相同。只需将条件视为迭代器是否具有更多元素而为true或false即可。


8
这是一个非常优秀的答案。把你的循环看作一系列的elif语句,else的行为就会展现其自然的逻辑。 - Nomenator
1
我也喜欢这个答案,但它没有与一系列elif语句进行类比。有一个回答与之类比,并且有一个净赞成票。 - alexis
3
不完全准确,while循环可以在“break”之前满足False条件,这种情况下“else”将不会运行,但条件为False。同样,在for循环中,它可以在最后一个元素上“break”。 - Tadhg McDonald-Jensen

36
更好的思考方式是:如果在前面的for块执行完毕(达到耗尽状态)且没有出现异常、中断或返回语句等控制语句,那么else块将总是被执行。
在这个上下文中,“正常情况”指的是没有异常breakreturn等控制语句。任何从for中窃取控制权的语句都会导致else块被跳过。
常见用例是在iterable中搜索项时,当找到该项或通过以下else块引发/打印"未找到"标志时,搜索终止:
for items in basket:
    if isinstance(item, Egg):
        break
else:
    print("No eggs in basket")  

continue语句不会从for循环中夺取控制权,所以当for循环完成后,程序将继续执行else语句。


22
听起来不错...但是当事情不顺利时,你会期望执行一个"else"子句,不是吗?我已经开始感到困惑了... - alexis
我必须不同意你的观点:“从技术上讲,它与其他每个else并不完全相似”,因为当for循环中的所有条件都不为True时,else会被执行,就像我在我的回答中所演示的那样。 - Tadhg McDonald-Jensen
@alexis 从您添加的链接中,考虑到这个else的词源,我认为我们将不得不接受现状 :) - Moses Koledoye
这个 else 的行为并不像它的名字那样简单。要理解它,你需要应用一些抽象概念,比如大家已经提出的 nobreak - Moses Koledoye
1
这与“事情顺利进行”没有任何关系。当if/while条件计算为false或for超出项目时,else纯粹被执行。break退出包含的循环(在else之后)。continue返回并重新评估循环条件。 - Mark Tolonen
显示剩余5条评论

33
if语句在其条件为假时执行else分支,while/else同理。因此,你可以将while/else视为只要其真实条件持续为真就一直运行的if语句,直到条件变为假值。使用break指令并不会改变这个过程,它只是跳出循环,而不进行评估。只有在if/while条件为假时才会执行else分支。 for语句类似,只是它的假值条件是迭代器耗尽。 continuebreak不执行else分支,这不是它们的功能。 break用于退出当前循环,continue则返回到包含循环的顶部,重新评估条件。只有通过评估if/while条件为假(或者for没有更多条目)才会执行else分支,没有其他方式。

1
你说的听起来很有道理,但把三个终止条件“直到[条件]为False或中断/继续”归为一类是错误的:关键是,如果循环被continue(或正常)退出,则会执行else子句,但不会在我们使用break退出时执行。这些微妙之处就是我试图真正理解else捕获什么以及它不捕获什么的原因。 - alexis
4
是的,我需要澄清一下。编辑过了。continue语句不会执行else语句,但会返回到循环的顶部,然后可能会评估为false。 - Mark Tolonen

24

这是其实意思:

for/while ...:
    if ...:
        break
if there was a break:
    pass
else:
    ...

这是一种更好的写法,适用于这个常见的模式:

found = False
for/while ...:
    if ...:
        found = True
        break
if not found:
    ...
return语句会直接离开函数,因此如果出现return语句,则不会执行else子句。唯一的例外是finally子句,其目的是确保始终执行。对于此问题,continue没有任何特殊作用,它只会导致当前循环迭代结束,可能会导致整个循环结束,在这种情况下,循环并不是通过break结束的。 try/else类似:
try:
    ...
except:
    ...
if there was an exception:
    pass
else:
    ...

20
如果您将您的循环视为类似于这个结构(有点类似伪代码):
loop:
if condition then

   ... //execute body
   goto loop
else
   ...

可能更容易理解的是,循环本质上只是一个重复执行直到条件为falseif语句。这是一个重要的观点。循环检查它的条件,发现它是false,因此执行else(就像普通的if/else一样),然后循环结束。

因此,请注意else 只有在检查条件时才会被执行。这意味着,如果您在执行过程中通过例如returnbreak退出循环体,由于条件不再被检查,else分支将不会被执行。

另一方面,continue会停止当前的执行,并跳回重新检查循环的条件,这就是为什么在这种情况下可以到达else的原因。


我非常喜欢这个答案,但你可以简化:省略 end 标签,只需将 goto loop 放在 if 体内。甚至可以通过将 if 放在标签的同一行来取消缩进,它看起来就非常像原始代码了。 - Bergi
@Bergi 是的,我认为这使得问题更加清晰了,谢谢。 - Keiwan

16

我和循环的else语句的那个“抓住你”的时刻是在看Raymond Hettinger的一个演讲时,他讲了一个关于他如何认为它应该被称为nobreak的故事。看一下下面的代码,你认为它会做什么?

for i in range(10):
    if test(i):
        break
    # ... work with i
nobreak:
    print('Loop completed')

你会猜它做什么吗?那么,如果在循环中没有遇到 break 语句,nobreak 部分才会被执行。


8
通常我会这样想循环结构:

for item in my_sequence:
    if logic(item):
        do_something(item)
        break

这与变量数量的 if/elif 语句非常相似:

if logic(my_seq[0]):
    do_something(my_seq[0])
elif logic(my_seq[1]):
    do_something(my_seq[1])
elif logic(my_seq[2]):
    do_something(my_seq[2])
....
elif logic(my_seq[-1]):
    do_something(my_seq[-1])

在这种情况下,for循环中的else语句的作用与elif链上的else语句完全相同,只有在它之前的所有条件都不为True时才会执行。(或者通过return或异常中断执行) 如果我的循环不符合这个规范,通常我选择放弃使用"for: else",原因正是你提出这个问题的原因:它不直观。

1
没错,但是循环会运行多次,所以不太清楚你是如何将其应用于for循环的。你能澄清一下吗? - alexis
@alexis 我重新修改了我的回答,我认为现在更加清晰易懂了。 - Tadhg McDonald-Jensen

7
其他人已经解释了while/for...else的机制,而Python 3语言参考手册有权威的定义(请参见whilefor),但以下是我个人的助记符。我认为关键在于将其分解为两个部分:一个用于了解与循环条件相关的else的含义,另一个用于理解循环控制。
我发现最容易理解的是先理解while...else

while还有更多项时,做一些事情,否则如果你用完了,就做这个。

for...else的助记符基本上也是一样的:

for每个项目,做一些事情,但是如果你用完了,就做这个

在这两种情况下,只有在没有更多要处理的项目且最后一个项目已按常规方式处理(即没有breakreturn)时,才会到达else部分。一个continue只是回去查看是否还有更多的项目。对于这些规则,我的助记符适用于whilefor

breakreturn时,没有其他事情需要做,
当我说continue时,那就是为你“循环回到起点”

“回到起点”,显然是指我们检查可迭代对象中是否还有更多项的循环的开始,因此就else而言,continue实际上根本不起作用。

4
我建议可以通过以下方式加以改进:通常,使用for/else循环的目的是检查项目,直到找到所需项目并希望停止,或者直到项目已经用尽。"else" 的存在是为了处理“项目已经用尽(但没有找到想要的项目)”的情况。 - supercat
@supercat:可能是这样,但我不知道最常见的用途是什么。else也可以用于在完成所有项目时执行某些操作。例如编写日志条目、更新用户界面或向其他进程发出信号表示已完成。实际上,任何操作都可以。此外,一些代码片段的“成功”情况会在循环内部以break结束,而else用于处理“错误”情况,在迭代过程中没有找到合适的项目(也许这就是你想到的?)。 - Fabian Fagerholm
1
我所考虑的情况恰好是成功情况以“break”结束,“else”处理失败情况。如果循环中没有“break”,那么“else”代码也可以作为封闭块的一部分跟随循环。 - supercat
除非你需要区分循环在没有中断的情况下(这是一个成功的情况)遍历了所有可迭代项和循环没有遍历完所有可迭代项的情况,否则你必须将“完成”代码放在循环的else块中,或者使用其他方法跟踪结果。我基本上同意,我只是说我不知道人们如何使用这个功能,因此我想避免假设“else处理成功的情况”场景或“else处理失败的情况”场景更常见。但你说得对,所以我的评论被点赞了! - Fabian Fagerholm

7
测试驱动开发 (TDD) 中,当使用 转化优先原则 范式时,您将循环视为条件语句的一般化。
如果仅考虑简单的 if/else (没有 elif)语句,则此方法与此语法结合得很好。
if cond:
    # 1
else:
    # 2

概括为:

while cond:  # <-- generalization
    # 1
else:
    # 2

我是一名有用的助手,可以翻译文本。

在其他语言中,TDD从单个案例到需要集合的案例的步骤需要更多的重构。


这里有一个来自8thlight blog的例子:

在8thlight博客的链接文章中,考虑了Word Wrap kata:将换行符添加到字符串(下面代码片段中的s变量)中,以使它们适合给定的宽度(下面代码片段中的length变量)。 在某一点上,实现如下(Java):

String result = "";
if (s.length() > length) {
    result = s.substring(0, length) + "\n" + s.substring(length);
} else {
    result = s;
}
return result;

下一个目前失败的测试是:

@Test
public void WordLongerThanTwiceLengthShouldBreakTwice() throws Exception {
    assertThat(wrap("verylongword", 4), is("very\nlong\nword"));
    }

因此,我们有一个在特定条件下工作的代码:当满足特定条件时,会添加一个换行。 我们希望改进代码以处理多个换行。文章中提出的解决方案建议应用 (if->while) 转换,但作者发表了以下评论:

While 循环不能有 else 语句,因此我们需要通过在 if 语句路径中减少执行内容来消除 else 路径。同样,这是一种重构。

这迫使我们在一个失败的测试环境中对代码进行更多的更改:

String result = "";
while (s.length() > length) {
    result += s.substring(0, length) + "\n";
    s = s.substring(length);
}
result += s;

在TDD中,我们希望尽可能少地编写代码来使测试通过。由于Python的语法,可以进行以下转换:
从:
result = ""
if len(s) > length:
    result = s[0:length] + "\n"
    s = s[length:]
else:
    result += s

到:

result = ""
while len(s) > length:
    result += s[0:length] + "\n"
    s = s[length:]
else:
    result += s

6
我个人的理解是,当你遍历完循环时,else:语句块会被执行。
如果你使用breakreturnraise语句,则不会遍历完整个循环,它们会立即停止循环,因此else:语句块不会被执行。如果你使用continue语句,循环仍然会进行到最后一次迭代,因为continue只是跳过当前迭代而已,并没有停止整个循环。

1
我喜欢这个,我认为你有些东西。它与旧的循环关键字之前如何实现循环有些联系(即:在循环底部放置检查,并在成功时使用“goto”返回循环顶部)。但它是被投票最高答案的一个更短的版本... - alexis
@alexis,这是主观的,但我发现我的表达方式更容易思考。 - Winston Ewert
其实我同意。仅仅因为它更简洁。 - alexis

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