Skip to content
This page has been auto-translated and may contain errors.View in English

控制流

到目前为止,你写的每个程序都以相同的方式运行:自上而下,一次一行。这对简单的脚本来说是可行的,但真正的程序需要做出决策并重复工作。一个测验需要检查答案是否正确。一个游戏需要持续运行,直到玩家赢或输。本章介绍如何让你的程序进行分支和重复。

控制流塑造了程序的执行路径。条件(if/elif/else)在分支之间进行选择。循环(whilefor)重复代码块。Python 的 for 循环是一个迭代器协议,而不是基于索引的计数器。理解语法和底层模型可以让你的代码更简洁。

Python 的控制流原语是 if/elif/elsewhileforfor 在可迭代对象上调用 __iter__,并不断调用 next() 直到出现 StopIterationwhile 通过 __bool____len__ 来评估条件对象。breakcontinue 仅影响最近的封闭循环;loop-else 仅在循环未遇到 break 而正常结束时运行。

比较

在做出决定之前,你需要比较事物。比较运算符返回 TrueFalse。最重要的一点要尽早搞清楚:= 用于赋值,== 用于检查两个值是否相等。混淆它们是最常见的初学者错误之一。

比较运算符会调用相应的双下划线方法(__eq____lt__ 等)并返回一个 bool。Python 支持链式比较:0 < x < 10 会被求值为 0 < x and x < 10,而不是像大多数语言那样从左到右求值。字符串比较是基于 Unicode 码点的字典序比较。

比较运算符调用富比较方法:__eq____ne____lt____le____gt____ge__。Python 支持短路的链式比较:a < b < c 等价于 a < b and b < c,但 b 只被求值一次。is 检查对象身份(id() 相等),而非值相等;值比较使用 ==,仅在比较 NoneTrueFalse 时使用 is

python
5 > 3     # True
5 < 3     # False
5 == 5    # True   (注意:双等号;= 是赋值,== 是比较)
5 != 3    # True   ("不等于")
5 >= 5    # True   ("大于或等于")
5 <= 4    # False  ("小于或等于")

=== 的区别几乎让所有人在初期都会犯错。赋值(=)用于存储值;比较(==)用于检查两个值是否相同。

你也可以比较字符串。Python 会按字母顺序比较它们:

python
"apple" == "apple"   # True
"apple" < "banana"   # True  (a 在 b 前面)
"apple" == "Apple"   # False (区分大小写)

组合条件

andornot 用于组合比较。and 要求两边都为真。or 要求至少一边为真。not 会翻转结果。这些可以让你表达现实世界的条件,例如 "分数及格 AND 用户处于活动状态"。

andor 是短路求值的:and 在第一个 falsy 操作数处停止,or 在第一个 truthy 操作数处停止。它们返回实际的操作数值,而不仅仅是 TrueFalsenot 会在操作数上调用 __bool__ 并返回翻转后的结果。

and 从左到右求值,并返回第一个 falsy 操作数;如果所有操作数都为 truthy,则返回最后一个操作数。or 返回第一个 truthy 操作数;如果所有操作数都为 falsy,则返回最后一个。这种短路行为是一种保证:如果左侧已经决定了结果,则不会对右侧求值。not x 等价于 True if not bool(x) else False,但 Python 对其进行了优化。

python
age   = 25
score = 88

age >= 18 and score >= 80    # True  (两边都必须为真)
age < 18 or score >= 80      # True  (至少一边必须为真)
not age >= 18                # False (翻转结果)

and 要求两边都为真。or 要求至少一边为真。not 取反。

真值与假值

Python 中的每个值都有布尔解释,即使它不是 TrueFalse。空字符串、零、空列表和 None 在条件中都表现得像 False。其他一切都表现得像 True。这意味着 if results: 可以检查列表是否非空,而无需写 if len(results) > 0:

Python 的真值规则:falsy 值有 False00.0""[](){}set()None。其他一切都是 truthy。条件会在对象上调用 __bool__,如果 __bool__ 未定义则回退到 __len____len__ 返回零的对象是 falsy。

真值测试调用 __bool__。如果 __bool__ 未定义,Python 会回退到 __len____len__ == 0 的对象是 falsy。自定义类通过实现这两个方法之一来控制真值。标准的 falsy 值有:False00.00j""b""[](){}set()frozenset()None,以及任何 __bool__ 返回 False__len__ 返回 0 的对象。

python
# 以下这些在条件中都表现得像 False:
False, 0, 0.0, "", [], {}, (), None

# 其他一切都表现得像 True

这意味着 if results: 是一种自然的表达方式,用来表示 "如果列表不为空",而 if name: 可以检查字符串是否有任何内容。

if / elif / else

if 语句仅在其条件为 True 时运行一段代码块。elif 在第一个条件为假时添加更多要检查的条件。else 捕获所有未匹配任何条件的情况。Python 使用缩进而不是大括号来定义每个块内的内容。

if/elif/else 自上而下评估条件,并运行第一个匹配的块。Python 使用缩进(按约定为 4 个空格)来定义块的作用域;不一致的缩进会导致 SyntaxError。只有一个分支会运行:一旦某个条件匹配,所有后续的 elifelse 都会被跳过。

Python 使用缩进作为块分隔符,由解析器强制执行。解释器会为每个分支生成 SETUP_BLOCK 字节码;恰好有一个分支会被执行。elif 是语法糖:它避免了一系列裸 if 语句所造成的嵌套。每个条件都是惰性求值的,仅当所有前面的条件都为 falsy 时才会求值。

python
score = 87

if score >= 90:
    print("A grade")
elif score >= 80:
    print("B grade")
elif score >= 70:
    print("C grade")
else:
    print("Below C")

规则:

  • if 是必需的,并且总是放在最前面
  • elif("else if" 的缩写)是可选的,可以有任意多个
  • else 是可选的,用于处理所有未匹配的情况,并且放在最后
  • Python 使用缩进(4 个空格)来标记每个块内的内容;没有大括号

缩进不是可选的或装饰性的。Python 使用它来定义结构。不一致的缩进会导致语法错误。

单行条件

对于简单的是/否赋值,Python 有一种紧凑的单行形式,称为三元表达式value_if_true if condition else value_if_false。仅在逻辑确实简单且读起来像一句话时使用它。

条件表达式(三元运算符)根据条件求值为两个值之一。它是一个表达式而不是语句,因此可以出现在任何需要值的地方:在 f-string 中、作为函数参数、在赋值中。在简单的是/否情况下使用它;对于涉及 elif 的情况,请编写完整版本。

条件表达式 x if condition else y 是一个单一表达式,它评估条件并返回 xy,而不执行另一个分支。它映射到 POP_JUMP_IF_FALSE 字节码的组合。与 if/else 块不同,它不能跨多个语句,也不能包含 elif;对于复杂的分支,完整的 if/elif/else 块更清晰。

python
label = "pass" if score >= 50 else "fail"

这是一个三元表达式;它读起来像一句话。当逻辑确实简单时使用它。对于涉及 elif 的任何情况,请编写完整版本。

while 循环

只要条件为 Truewhile 循环就会重复执行其代码块。当你不知道循环应该运行多少次时使用它,例如等待有效输入或重试直到任务成功。

while 在每次迭代之前评估其条件,仅当条件为 truthy 时才运行代码块。对于退出条件依赖于循环内部某些变化的循环,使用它。当迭代次数已知,或者你在迭代一个集合时,for 通常更简洁。

while 在每次迭代之前对条件表达式调用 __bool__(或 __len__)。循环体可以修改条件。当退出条件必须在循环体的中间或末尾求值时,带有内部 breakwhile True 是惯用的 "loop-until" 模式。缺少 break 的无限循环是常见的挂起源头。

python
lives = 3

while lives > 0:
    print(f"Lives remaining: {lives}")
    lives -= 1

print("Game over")

当你不知道循环会运行多少次时,while 是最好的。当你知道次数,或者你在迭代一个集合时,for 更简洁。

break 和 continue

break 会立即退出循环,无论还剩多少次迭代。continue 会跳过当前迭代的剩余部分,并跳回到条件检查处。两者都只影响它们所在的最内层循环。

break 终止最近的封闭循环,将控制权转移到它之后的第一条语句。continue 跳过当前循环体的剩余部分,并从条件检查处重新开始(或在 for 循环中进入下一次迭代)。两者都只影响最内层的封闭循环。

break 发出 BREAK_LOOP 字节码,退出循环的代码块并跳过任何关联的 else 子句。continue 发出 CONTINUE_LOOP(或根据上下文发出 JUMP_ABSOLUTE),从循环头重新开始。两者都仅作用于最近的封闭循环;Python 中没有带标签的 break。要从嵌套循环中跳出,请使用布尔标志或重构为带 return 的函数。

break 会立即退出循环:

python
target = 5
num    = 0

while True:
    num += 1
    if num == target:
        print(f"Found {target}")
        break   # 停止循环

当退出条件复杂或需要在循环体末尾发生时,带有 breakwhile True: 是一种有效且常见的模式。

continue 会跳过当前迭代的剩余部分,并返回到条件检查:

python
num = 0

while num < 10:
    num += 1
    if num % 2 == 0:
        continue    # 跳过偶数
    print(num)      # 只打印奇数:1, 3, 5, 7, 9

for 循环

for 循环每次遍历序列中的一个元素:一个列表、一个字符串、一个数字范围。你在 for 后命名的变量依次接收每个元素。你不需要自己管理计数器或检查长度。

for 在可迭代对象上调用 iter() 以获取迭代器,然后在其上调用 next(),直到出现 StopIteration。这意味着 for 适用于任何实现迭代器协议的对象:列表、字符串、字典、文件、范围以及自定义对象。它不限于索引序列。

for target in iterable 调用 iter(iterable) 来获取迭代器对象,然后重复调用 next(iterator) 并将结果绑定到 target,直到引发 StopIteration。for 循环在内部捕获 StopIteration。任何实现 __iter__(或同时实现 __iter____next__)的对象都是可迭代的。这包括生成器和文件对象等惰性对象。

python
players = ["小明", "小红", "小刚"]

for player in players:
    print(f"Hello, {player}!")

for 循环也适用于字符串(逐字符迭代)和任何其他序列类型。

range()

range() 生成一个数字序列供你循环。range(5) 会给你 0, 1, 2, 3, 4。你可以控制起始、结束和步长。当你需要循环运行特定次数时使用它。

range(start, stop, step) 生成从 start 到(但不包括)stop 的整数,步长为 step。它是一个惰性序列:它不创建列表,而是按需生成数字。这使得 range(10_000_000) 在内存上是高效的。所有三种形式都接受负参数以反向计数。

range 是一个类型,而不是函数。range(n) 创建一个 range 对象,它以 O(1) 的复杂度计算成员关系和索引,而无需实例化序列。它支持 len()in、切片和反向迭代。在内部它只存储 start、stop 和 step。当你只需要迭代时,请优先使用它而不是 list(range(n))

python
for i in range(5):
    print(i)    # 0, 1, 2, 3, 4

range() 有三种形式:

调用它生成什么
range(5)0, 1, 2, 3, 4
range(2, 6)2, 3, 4, 5
range(0, 10, 2)0, 2, 4, 6, 8(步长为 2)
range(5, 0, -1)5, 4, 3, 2, 1(倒计数)

range() 不会创建列表。它一次生成一个数字,即使对于非常大的范围也很高效。

enumerate()

enumerate() 在你循环时同时给你索引和值,因此你不需要单独跟踪计数器。i, player 部分在每次迭代时自动接收一对值。

enumerate(iterable, start=0) 包装任何迭代器并产生 (index, value) 元组。start 参数偏移计数器,但不会更改底层索引。优先使用 enumerate() 而不是管理计数器变量;它更简洁,也不容易出错。

enumerate 将任何迭代器包装在一个 enumerate 对象中,该对象产生 (count, value) 对。start 参数设置初始计数器值。在 for 头中解包(for i, v in enumerate(...))有效,因为每个产生的元素都是一个元组。enumerate 每步是 O(1),除了计数器之外没有额外的内存分配。

python
players = ["小明", "小红", "小刚"]

for i, player in enumerate(players):
    print(f"{i + 1}. {player}")
# 1. 小明
# 2. 小红
# 3. 小刚

i, player 语法称为解包。Python 自动将 (index, value) 对拆分为两个名称。

默认情况下,enumerate() 从 0 开始。传入起始值来改变它:

python
for i, player in enumerate(players, start=1):
    print(f"{i}. {player}")    # 从 1 开始

嵌套循环

你可以把一个循环放在另一个循环里面。每次外层循环的单次迭代,内层循环都会完整地运行一遍。这就是处理网格、组合或任何具有两级结构的数据的方式。

对于外部长度 m 和内部长度 n 的嵌套循环,迭代次数为 O(m × n)。嵌套循环内部的 breakcontinue 只影响最内层的循环。要从多个层级中跳出,请使用标志变量或重构为函数。

每次 for 循环调用都会创建一个新的迭代器对象。嵌套循环独立地组合其迭代器。break 只退出最内层的循环;Python 中没有带标签的 break。常见的解决方法是使用标志变量,或者将内层循环封装在一个函数中并使用 return。对于笛卡尔积,itertools.product 比嵌套 for 循环更易读。

python
rows = [1, 2, 3]
cols = ["A", "B"]

for row in rows:
    for col in cols:
        print(f"{col}{row}", end=" ")
    print()   # 每行之后换行
# A1 B1
# A2 B2
# A3 B3

嵌套循环内部的 breakcontinue 只影响最内层的循环。

循环-else

Python 循环可以有一个 else 子句,它仅在循环未遇到 break 而结束时才运行。它不常用,但它是编写 "在列表中搜索,如果什么也没找到,就执行此操作" 最简洁的方式。

forwhile 循环上的 else 在循环正常完成时(耗尽可迭代对象或条件变为假)执行,而不是在遇到 break 时执行。它是 "搜索并报告未找到" 的惯用模式,无需单独的 found 标志变量。

循环 elseBREAK_LOOP 字节码控制:如果触发了 break,则不会进入 else 块的设置代码。如果循环耗尽,则会运行 else 块。这在语义上不同于 if 上的普通 else。主要的实际用途是带 break 的搜索模式;在此之外,它很少见,可能会让不熟悉它的读者感到困惑。

python
target = "小华"
names  = ["小明", "小红", "小刚"]

for name in names:
    if name == target:
        print(f"Found {target}")
        break
else:
    print(f"{target} not in list")   # 会运行,因为 break 从未触发

如果 break 运行了,else 会被跳过。如果循环耗尽了序列,else 会运行。这是一种小众模式,但比标志变量更简洁。

排序

sorted() 返回一个新的排序列表,并保持原始列表不变。.sort() 在原地对列表进行排序并返回 Nonekey= 参数允许你按原始值以外的内容进行排序。例如,不区分大小写地对名称进行排序,或按分数对玩家元组进行排序。

sorted() 是安全的默认选择:它从不修改原始数据。.sort() 在原地修改并返回 None。两者都接受 reverse=True 以进行降序排序。key= 参数接受一个在比较之前应用于每个元素的函数。这将排序标准与数据分离。

两者都使用 Timsort:最坏情况 O(n log n),几乎已排序数据 O(n),稳定。.sort() 故意返回 None(命令-查询分离)。key= 函数每个元素调用一次,而不是每次比较调用一次,所以昂贵的 key 计算不会被重复。key=str.lower 是一个未绑定方法引用;key=lambda p: p[1] 是一个用于访问特定字段的内联函数。

python
scores = [87, 42, 96, 55, 71]

ranked = sorted(scores)           # [42, 55, 71, 87, 96](新列表)
scores.sort()                     # 对原始列表排序,返回 None
scores.sort(reverse=True)         # [96, 87, 71, 55, 42]

两者都接受 key= 参数:在比较之前应用于每个项目的函数:

python
names = ["小刚", "小明", "小红"]
sorted(names, key=str.lower)       # 不区分大小写的排序

players = [("小明", 87), ("小红", 96), ("小刚", 55)]
sorted(players, key=lambda p: p[1])   # 按分数排序

什么是 lambda?

lambda p: p[1] 是一个单行函数。它接受一个玩家元组并返回分数。Lambda 函数将在 Lambda、推导式和 zip 一章中介绍。

对于简单情况,使用 sorted()。对于想要原地修改的列表,使用 .sort()

实践中

遍历分数,累加总分,计算及格数,并打印摘要:

python
raw_scores = [87, 42, 96, 55, 71, 63]

total   = 0
passing = 0

for score in raw_scores:
    total += score
    if score >= 60:
        passing += 1

average = total / len(raw_scores)
print(f"Average: {average:.1f}")
print(f"Passing: {passing}/{len(raw_scores)}")
print(f"Top score: {sorted(raw_scores, reverse=True)[0]}")

按排序顺序处理文件列表,跳过过大的文件,并报告跳过了多少个:

python
files = [
    {"name": "report_jan.csv", "size_mb": 12},
    {"name": "report_feb.csv", "size_mb": 850},
    {"name": "report_mar.csv", "size_mb": 7},
]

MAX_SIZE = 100
skipped  = 0

for f in sorted(files, key=lambda x: x["name"]):
    if f["size_mb"] > MAX_SIZE:
        print(f"Skipping {f['name']} ({f['size_mb']} MB, too large)")
        skipped += 1
    else:
        print(f"Processing {f['name']}...")

print(f"\nDone. {skipped} file(s) skipped.")

扫描请求日志查找错误,然后使用一个在成功或达到尝试次数限制时退出的重试循环:

python
requests = [
    {"method": "GET",  "path": "/users",  "status": 200},
    {"method": "POST", "path": "/users",  "status": 201},
    {"method": "GET",  "path": "/broken", "status": 500},
]

errors = []

for req in requests:
    if req["status"] >= 400:
        errors.append(req)

if errors:
    print(f"{len(errors)} error(s) in request log:")
    for err in errors:
        print(f"  {err['method']} {err['path']} -> {err['status']}")
else:
    print("All requests succeeded")

attempts    = 0
max_retries = 3
success     = False

while attempts < max_retries and not success:
    attempts += 1
    print(f"Attempt {attempts}...")
    success = attempts >= 2   # 模拟第二次尝试成功

print("Connected" if success else "Failed after all retries")