Python 基础体系 · 第 10/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。

Python 控制流:if、for、while、break、else 与作用域

控制流决定程序“下一步执行哪一段代码”。在 Python 中,if 负责条件选择,forwhile 负责重复执行,breakcontinue 改变循环路径,else 根据某个控制流是否“正常完成”决定是否执行,而作用域决定这些语句中创建和读取的名称到底指向哪里。

理解这些语法不能只记住形式。更重要的是建立三个模型:

  1. 条件模型:表达式如何被判断为真或假。
  2. 循环模型:循环如何取得下一项、何时回到条件、何时结束。
  3. 名称模型:赋值产生的名称属于哪个作用域,以及控制流是否会改变这个事实。

1. 先区分代码块、子句和作用域

Python 的复合语句由一个或多个子句组成。子句包含:

  • 一个以关键字开头、以冒号结尾的头部;
  • 一个由缩进标识的语句组,称为 suite,通常译为“代码块”。

例如:

if score >= 60:
    print("及格")
else:
    print("不及格")

这里有两个子句:

  • if score >= 60: 子句;
  • else: 子句。

缩进不是装饰,而是语法的一部分。Python 通过缩进确定哪个语句属于哪个子句,因此不会出现 C 风格语言中常见的“悬空 else”问题。一个 else 总是按照缩进归属于对应的 ifforwhiletry。(docs.python.org)

但需要特别区分:

代码块不一定创建作用域。

ifforwhile 的代码块是控制流代码块,却不会像函数那样创建新的局部作用域:

if True:
    x = 10

print(x)  # 10
for item in [1, 2, 3]:
    last = item

print(last)  # 3

因此,后文会反复看到一个重要事实:

  • ifforwhile 会改变“哪些语句执行”;
  • 但它们通常不会改变“赋值名称属于哪个作用域”。

函数、类和模块才是理解普通名称绑定时更重要的边界。Python 语言参考将函数、类定义和模块顶层视为代码块;在代码块中发生的赋值会影响名称解析。(docs.python.org)


2. 条件表达式与布尔真值

2.1 if 判断的不是“必须为布尔值的表达式”

if 的条件表达式可以返回任意对象。Python 会根据对象的真值判断是否执行分支:

if value:
    ...

等价于询问:

bool(value)

常见的假值包括:

False
None
0
0.0
""
[]
()
{}
set()

非空容器、非零数字以及大多数普通对象通常是真值。

例如:

items = []

if items:
    print("有数据")
else:
    print("没有数据")

输出:

没有数据

这里并不是因为 items 等于 False,而是因为空列表的布尔值为 False

如果一个对象定义了 __bool__(),Python 会调用它;如果没有定义,则可能通过 __len__() 判断长度是否为零。工程代码中应注意:条件判断可能触发用户自定义代码,因此不应假设所有条件判断都只是一次廉价的字段读取。

2.2 ifelifelse 的执行规则

语法结构可以抽象为:

if 条件1:
    代码块1
elif 条件2:
    代码块2
elif 条件3:
    代码块3
else:
    默认代码块

执行过程是:

  1. 求值 条件1
  2. 如果为真,执行 代码块1,整个 if 语句结束;
  3. 如果为假,求值 条件2
  4. 继续向下寻找第一个为真的条件;
  5. 如果所有条件都为假,执行 else
  6. 如果没有 else,什么都不执行。

规范保证的是:最多执行一个分支。不仅其他分支的代码不会执行,其中的表达式也不会被求值。(docs.python.org)

def check(name):
    print(f"检查 {name}")
    return name == "B"

if check("A"):
    print("分支 A")
elif check("B"):
    print("分支 B")
elif check("C"):
    print("分支 C")
else:
    print("默认分支")

输出:

检查 A
检查 B
分支 B

check("C") 没有被调用,因为前一个 elif 已经成功。

这与多个独立的 if 不同:

value = 10

if value > 0:
    print("正数")

if value < 20:
    print("小于 20")

输出:

正数
小于 20

两个 if 是两个独立的选择;而 ifelifelse 构成一个互斥选择结构。

2.3 条件表达式的短路求值

if 中经常使用 andor

if user is not None and user.is_active:
    ...

and 从左到右求值。左侧为假时,右侧不会求值:

user = None

if user is not None and user.name == "Alice":
    print("匹配")

因为 user is not None 为假,user.name 不会执行,因此不会产生 AttributeError

or 则在左侧为真时停止:

name = provided_name or "anonymous"

如果 provided_name 是空字符串,结果会使用 "anonymous"。这不是“只判断是否为 None”,而是判断真值。如果业务语义需要区分空字符串和缺失值,应明确写出:

name = "anonymous" if provided_name is None else provided_name

3. for:遍历迭代器,而不是自动递增计数器

3.1 for 的核心不是数字范围

Python 的 for 用来遍历可迭代对象:

for item in iterable:
    ...

可迭代对象是能够提供一系列元素、直到元素耗尽的对象。列表、元组、字符串、字典、集合、文件对象和生成器都可以被遍历。range() 也属于可迭代对象,但它不会一次性创建完整列表。(docs.python.org)

for char in "abc":
    print(char)

输出:

a
b
c
for key in {"a": 1, "b": 2}:
    print(key)

输出的是字典的键:

a
b

如果需要键和值,应显式遍历 items()

data = {"a": 1, "b": 2}

for key, value in data.items():
    print(key, value)

3.2 for 的概念执行过程

下面的代码:

for item in iterable:
    process(item)

可以用近似语义理解为:

iterator = iter(iterable)

while True:
    try:
        item = next(iterator)
    except StopIteration:
        break
    process(item)

这不是要求实现必须采用完全相同的 Python 源码,而是帮助理解 for 的数据流:

  1. iterable 表达式求值一次;
  2. Python 从它获得一个迭代器;
  3. 每次调用迭代器获取下一项;
  4. 将该项赋值给目标变量;
  5. 执行循环体;
  6. 循环体结束后获取下一项;
  7. 迭代器耗尽时正常结束。

语言参考明确规定,for 后面的可迭代表达式只求值一次,然后创建迭代器;每得到一个元素,就按普通赋值规则绑定到目标列表。(docs.python.org)

因此,下面的 make_items() 只调用一次:

def make_items():
    print("创建可迭代对象")
    return [10, 20, 30]

for value in make_items():
    print("处理", value)

输出:

创建可迭代对象
处理 10
处理 20
处理 30

3.3 循环变量会被后续迭代覆盖

循环体内对循环变量的赋值,不会改变下一轮 for 的赋值过程:

for i in range(3):
    print("进入循环时:", i)
    i = 100
    print("手动修改后:", i)

输出:

进入循环时: 0
手动修改后: 100
进入循环时: 1
手动修改后: 100
进入循环时: 2
手动修改后: 100

每一轮开始时,for 都会把下一个元素重新绑定给 i。语言参考也明确说明,目标列表中的名称会被后续元素覆盖;循环结束后,目标名称不会自动删除。(docs.python.org)

如果迭代对象为空,循环变量甚至不会被绑定:

for item in []:
    pass

print(item)

结果:

NameError: name 'item' is not defined

但如果循环至少执行过一次:

for item in [1, 2, 3]:
    pass

print(item)  # 3

这正是“循环块不创建新作用域”的表现。

3.4 不要在遍历同一个容器时随意修改它

下面的代码容易产生错误结果:

numbers = [1, 2, 3, 4, 5, 6]

for number in numbers:
    if number % 2 == 0:
        numbers.remove(number)

print(numbers)

输出可能是:

[1, 3, 5]

但这种写法依赖列表内部索引变化,容易漏掉相邻元素。例如:

numbers = [2, 4, 6]

for number in numbers:
    numbers.remove(number)

print(numbers)

结果是:

[4]

第一次删除 2 后,4 移到当前位置;迭代器继续向后移动,于是跳过了 4,处理 6,最终留下 4

更明确的做法是遍历副本:

numbers = [2, 4, 6]

for number in numbers.copy():
    numbers.remove(number)

print(numbers)

或者直接创建新容器:

numbers = [1, 2, 3, 4, 5, 6]
odd_numbers = [number for number in numbers if number % 2 != 0]

print(odd_numbers)

Python 官方教程也建议,在修改集合时遍历副本,或者构造新的集合。(docs.python.org)

3.5 range() 的边界

range() 表示算术序列,结束值不包含在内:

list(range(2, 6))

结果:

[2, 3, 4, 5]

其一般形式是:

range(start, stop, step)

生成的值满足:

start + n * step

并且在 step > 0 时小于 stop,在 step < 0 时大于 stop

例如:

list(range(10, 2, -2))

结果:

[10, 8, 6, 4]

range(5) 等价于从 0 开始、步长为 1、结束值为 5,即产生 04


4. while:重复检查条件,直到条件为假

4.1 while 的执行顺序

while condition:
    body

执行过程是:

  1. 求值 condition
  2. 如果为假,循环立即结束;
  3. 如果为真,执行 body
  4. body 执行完毕后回到第 1 步。

因此,while 的代码块可能一次也不执行:

count = 0

while count > 0:
    print("执行循环体")

由于第一次检查时 count > 0 为假,没有任何输出。

while 的条件必须在循环体中发生变化,否则可能形成无限循环:

count = 3

while count > 0:
    print(count)
    count -= 1

输出:

3
2
1

循环状态可以表示为:

循环前 条件 循环体动作 下一状态
count = 3 打印并减一 count = 2
count = 2 打印并减一 count = 1
count = 1 打印并减一 count = 0
count = 0 不执行 结束

for 主要依赖“迭代器是否还有下一个元素”不同,while 主要依赖程序员维护的状态变量或外部状态。

4.2 while True 与显式退出

交互式输入、重试和事件循环经常使用:

while True:
    command = input("> ")

    if command == "quit":
        break

    print(f"执行命令: {command}")

这里的终止条件不放在 while 头部,而是通过 break 表达。这样可以把“读取一次输入”和“决定是否结束”放在同一个循环体中。

但是,while True 只有在所有路径都能最终退出时才是可控的。以下代码如果网络请求持续失败,就会无限重试:

while True:
    try:
        connect()
        break
    except ConnectionError:
        print("重试")

生产代码通常还需要重试次数、超时、退避策略和不可恢复错误判断,否则控制流正确,并不意味着系统行为正确。

4.3 海象运算符与 while

Python 支持在 ifwhile 条件中使用赋值表达式:

while chunk := read_chunk():
    process(chunk)

它可以理解为:

while True:
    chunk = read_chunk()
    if not chunk:
        break
    process(chunk)

赋值表达式返回被赋的值,因此当 read_chunk() 返回空值时,循环条件为假并结束。使用时应确认“空值”确实代表结束,而不是合法数据。


5. break:终止最近的一层循环

5.1 break 不终止 if

break 只能出现在语法上嵌套于 forwhile 的位置,并且终止的是最近的外层循环。它不能单独用于退出 if。(docs.python.org)

for number in range(10):
    if number == 3:
        break
    print(number)

输出:

0
1
2

执行到 number == 3 时,break 直接离开整个 for,而不是仅仅离开 if

5.2 嵌套循环中的 break

for outer in range(3):
    for inner in range(3):
        print(outer, inner)
        if inner == 1:
            break

输出:

0 0
0 1
1 0
1 1
2 0
2 1

break 只终止内层 for。外层循环继续执行。

如果需要同时退出多层循环,可以使用显式状态:

found = False

for row in matrix:
    for value in row:
        if value == target:
            found = True
            break
    if found:
        break

也可以封装到函数中,用 return 离开整个函数:

def find_value(matrix, target):
    for row in matrix:
        for value in row:
            if value == target:
                return row, value
    return None

这里 return 的语义比多层 break 更直接:找到目标后结束函数调用,而不仅是结束某一层循环。

5.3 break 退出时循环变量保留当前值

for number in range(10):
    if number == 4:
        break

print(number)

输出:

4

break 终止循环时,循环目标变量仍然保持最后一次绑定的值。(docs.python.org)

这可以用于简单搜索,但应避免把“循环变量是否存在”作为复杂业务状态。更清晰的写法是显式保存结果:

match = None

for number in range(10):
    if number == 4:
        match = number
        break

if match is None:
    print("未找到")
else:
    print("找到", match)

6. continue:跳过本轮剩余代码

虽然标题重点是 break,但理解循环控制流不能缺少 continue

continue 不会结束循环,而是跳过本轮循环体剩余部分,进入下一轮:

for number in range(6):
    if number % 2 == 0:
        continue
    print(number)

输出:

1
3
5

执行路径是:

  • number = 0:条件成立,跳到下一轮;
  • number = 1:条件不成立,打印;
  • number = 2:跳到下一轮;
  • 依次类推。

break 的区别是:

语句 作用
break 终止最近的一层循环
continue 终止当前这一轮,继续下一轮
return 终止当前函数
异常传播 离开当前控制结构,沿调用栈寻找处理者

在包含 try...finally 的循环中,breakcontinue 离开或转入下一轮之前,相关的 finally 仍会执行。(docs.python.org)


7. 循环 else:没有 break 才执行

7.1 循环 else 不是“最后一轮之后无条件执行”

forwhile 都可以带 else

for item in iterable:
    ...
else:
    ...

它的准确语义是:

如果循环正常完成,没有执行 break,就执行 else

对于 for,正常完成意味着迭代器耗尽;对于 while,正常完成意味着条件变为假。若循环通过 breakreturn 或异常提前离开,循环 else 不执行。(docs.python.org)

7.2 for...else 的搜索模型

def contains_even(numbers):
    for number in numbers:
        if number % 2 == 0:
            print("找到偶数:", number)
            break
    else:
        print("没有找到偶数")

测试:

contains_even([1, 3, 5])
contains_even([1, 4, 5])

输出:

没有找到偶数
找到偶数: 4

可以把它转换成等价的标志变量写法:

def contains_even(numbers):
    found = False

    for number in numbers:
        if number % 2 == 0:
            found = True
            print("找到偶数:", number)
            break

    if not found:
        print("没有找到偶数")

for...else 只是把“是否发生过 break”这个状态直接绑定到语法结构上。

7.3 空循环也会执行 else

for item in []:
    print("循环体")
else:
    print("else")

输出:

else

因为循环没有被 break,而是从一开始就已经正常耗尽。

while 同样如此:

ready = False

while ready:
    print("循环体")
else:
    print("条件一开始就是假")

输出:

条件一开始就是假

7.4 continue 不会阻止循环 else

for number in range(3):
    continue
else:
    print("循环正常结束")

输出:

循环正常结束

continue 只是跳过本轮,不是 break。只要后续没有 break,循环最终正常结束,else 就会执行。

7.5 质数判断中的 for...else

def is_prime(number):
    if number < 2:
        return False

    for divisor in range(2, number):
        if number % divisor == 0:
            break
    else:
        return True

    return False

for number in range(2, 10):
    print(number, is_prime(number))

输出:

2 True
3 True
4 False
5 True
6 False
7 True
8 False
9 False

number = 7

  1. 依次尝试除数 26
  2. 没有任何除数使余数为零;
  3. 没有执行 break
  4. 执行 else
  5. 返回 True

number = 6

  1. 除数为 2 时发现 6 % 2 == 0
  2. 执行 break
  3. 跳过循环 else
  4. 返回 False

这里的 else 可以读成“没有找到因数时”。


8. ifelse 与循环的 else不是同一种判断

下面两个 else 的触发条件完全不同:

if condition:
    ...
else:
    ...

它表示:

条件为假时执行 else

而:

for item in items:
    if condition(item):
        break
else:
    ...

它表示:

循环没有被 break 中断时执行 else

因此不要简单把循环 else 理解成“循环之后执行的代码”。下面的代码中,else 属于 for,不是 if

for number in range(3):
    if number == 10:
        break
else:
    print("for 正常结束")

输出:

for 正常结束

根据缩进,elsefor 对齐,而不是与 if 对齐。


9. 作用域:控制流不会创建新的普通局部作用域

9.1 作用域与代码块

作用域决定名称在什么范围内可见。对于普通 Python 代码,可以先使用以下模型:

  • 局部作用域:当前函数;
  • 闭包作用域:外层函数;
  • 全局作用域:当前模块;
  • 内置作用域builtins 中的名称。

这通常简称为 LEGB:

Local → Enclosing → Global → Builtins

Python 语言参考的表述是:名称会在最近的可见外层作用域中解析;找不到名称时抛出 NameError,函数内引用尚未绑定的局部变量时通常抛出 UnboundLocalError。(docs.python.org)

ifforwhile 不会插入一个新的普通局部作用域:

def demo(flag):
    if flag:
        value = 42

    return value

调用:

print(demo(True))   # 42
print(demo(False))  # UnboundLocalError

第二次调用不是因为 if 创建了一个作用域,而是因为 value 的赋值分支没有执行,函数局部变量 value 没有实际绑定。

可通过初始化避免这种路径依赖:

def demo(flag):
    value = None

    if flag:
        value = 42

    return value

现在:

print(demo(True))   # 42
print(demo(False))  # None

9.2 函数中的赋值会影响整个函数作用域

Python 根据函数代码块中的名称绑定判断局部变量。只要函数体内某处对名称赋值,该名称在整个函数中通常都会被视为局部变量,即使执行路径尚未走到赋值语句。(docs.python.org)

count = 10

def show():
    print(count)
    count = 20

show()

结果:

UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

很多人会以为 print(count) 会读取模块级的 count。实际上,函数中出现了 count = 20,因此 count 被判定为局部变量;而执行 print(count) 时,这个局部变量还没有绑定值。

可以使用 global 明确表示要操作模块级名称:

count = 10

def update():
    global count
    count = 20

update()
print(count)  # 20

global 必须出现在使用该名称之前,并且会改变该名称在当前代码块中的解析方式。(docs.python.org)

不过,函数中更常见、更可控的写法是通过参数和返回值传递状态:

def update(count):
    return count + 10

count = update(count)

这样状态流动显式,不依赖隐藏的模块级可变变量。

9.3 nonlocal 修改外层函数变量

嵌套函数不能直接给外层函数的局部变量重新绑定:

def make_counter():
    count = 0

    def next_value():
        count += 1
        return count

    return next_value

调用时会出现:

UnboundLocalError

因为 next_value() 中的 count += 1 等价于:

count = count + 1

这使 count 成为 next_value() 的局部变量,但右侧读取发生在绑定之前。

使用 nonlocal 可以声明要修改最近外层函数作用域中的名称:

def make_counter():
    count = 0

    def next_value():
        nonlocal count
        count += 1
        return count

    return next_value

counter = make_counter()

print(counter())  # 1
print(counter())  # 2
print(counter())  # 3

nonlocal 要求对应名称已经存在于某个外层函数作用域中;如果不存在,会在编译阶段产生 SyntaxError。(docs.python.org)

这里的控制流和作用域共同作用:

  • make_counter() 执行一次,创建 count
  • 返回的 next_value 通过闭包持有这个名称;
  • 每次调用 next_value()nonlocal 允许重新绑定外层的 count
  • 该状态不会因为函数返回而消失,因为闭包仍然引用它。

10. 循环变量与闭包的迟绑定

循环变量不创建独立作用域,这会与闭包结合产生经典问题:

functions = []

for i in range(3):
    functions.append(lambda: i)

print([func() for func in functions])

结果是:

[2, 2, 2]

原因不是三个 lambda 各自保存了一个整数,而是三个函数都引用同一个外层名称 i。函数真正执行时才解析这个名称;循环结束后,i 的值为 2,所以三个函数都读取到 2

这就是常说的迟绑定:闭包保存的是名称的查找关系,而不是在定义时自动复制当前值。Python 文档也说明,自由变量的名称解析发生在运行时,因此名称在函数定义之后发生改变,调用时可能读取到新的值。(docs.python.org)

如果希望在每轮循环中固定当前值,可以使用默认参数:

functions = []

for i in range(3):
    functions.append(lambda i=i: i)

print([func() for func in functions])

输出:

[0, 1, 2]

这里 i=i 的右侧表达式在创建函数时求值,默认参数因此保存了当轮的对象引用。

也可以使用工厂函数创建新的函数作用域:

def make_function(value):
    def get_value():
        return value
    return get_value

functions = [make_function(i) for i in range(3)]

print([func() for func in functions])

输出同样是:

[0, 1, 2]

11. 推导式与循环变量的作用域差异

列表推导式看起来像压缩后的 for 循环:

squares = [number * number for number in range(4)]

它大致表示:

squares = []

for number in range(4):
    squares.append(number * number)

但作用域并不完全相同。现代 Python 中,推导式在隐式嵌套作用域中执行,推导式的目标变量不会泄露到外层作用域;最左侧 for 的可迭代表达式则在外层作用域求值。(docs.python.org)

number = "outer"

squares = [number * number for number in range(4)]

print(squares)  # [0, 1, 4, 9]
print(number)   # outer

而普通 for 会覆盖外层名称:

number = "outer"

for number in range(4):
    pass

print(number)  # 3

因此,下面两种写法不能仅从“看起来相似”推断作用域行为:

for x in range(3):
    pass

# x 存在,值为 2
[x for x in range(3)]

# x 不会因此绑定到外层

推导式适合表达“遍历、过滤、转换”的纯数据变换;如果循环体包含多个副作用、异常处理或复杂控制流,普通 for 通常更容易检查状态变化。


12. breakcontinuefinally

breakcontinue 穿过带有 finallytry 时,finally 会先执行:

for number in range(3):
    try:
        print("处理", number)
        break
    finally:
        print("清理")

输出:

处理 0
清理

控制顺序是:

  1. 进入循环;
  2. 执行 try
  3. 执行 break
  4. 在真正离开循环前执行 finally
  5. 离开循环。

这对于释放资源、恢复状态和关闭锁非常重要。

反过来,在 finally 中使用 returnbreakcontinue 可能覆盖原本的控制流,导致异常被丢弃或返回值被改变。Python 3.14 的编译器会针对这类写法发出 SyntaxWarning,因为它们容易隐藏错误。(docs.python.org)

例如:

def bad():
    try:
        raise RuntimeError("原始异常")
    finally:
        return "成功"

调用:

print(bad())

输出:

成功

原始异常被 finally 中的 return 抑制了。通常应让 finally 只负责清理,而不要在其中改变函数返回或循环退出语义。


13. 一个综合示例:批量处理、提前终止和作用域

下面的函数处理一批任务:

  • 跳过被禁用的任务;
  • 发现致命任务时停止;
  • 没有发现致命任务时执行循环 else
  • 返回处理结果。
def process_tasks(tasks):
    processed = []

    for task in tasks:
        if not task["enabled"]:
            continue

        if task["kind"] == "fatal":
            print("发现致命任务:", task["name"])
            break

        processed.append(task["name"])

    else:
        print("所有启用任务都已正常检查")

    return processed

输入:

tasks = [
    {"name": "prepare", "enabled": True, "kind": "normal"},
    {"name": "cache", "enabled": False, "kind": "normal"},
    {"name": "validate", "enabled": True, "kind": "normal"},
]

print(process_tasks(tasks))

执行过程:

  1. prepare 已启用且不是致命任务,加入 processed
  2. cache 未启用,执行 continue
  3. validate 已启用且不是致命任务,加入 processed
  4. 迭代器耗尽,没有执行 break
  5. 执行 else
  6. 返回 ["prepare", "validate"]

输出:

所有启用任务都已正常检查
['prepare', 'validate']

加入致命任务:

tasks = [
    {"name": "prepare", "enabled": True, "kind": "normal"},
    {"name": "validate", "enabled": True, "kind": "fatal"},
    {"name": "publish", "enabled": True, "kind": "normal"},
]

print(process_tasks(tasks))

输出:

发现致命任务: validate
['prepare']

此时:

  • publish 没有被处理;
  • break 终止了 for
  • 循环 else 不执行;
  • 函数继续执行 return processed

这段代码中,if 负责单次任务的分类,continue 负责跳过当前任务,break 负责终止批处理,else 负责表达“没有遇到致命任务”,而 processed 属于函数局部作用域。


14. 与 match 的边界:条件分支和模式匹配不是一回事

Python 还提供 matchcase,适合按结构匹配数据:

match value:
    case 0:
        ...
    case [x, y]:
        ...
    case {"status": "ok"}:
        ...

if 主要计算布尔条件;match 则尝试让主题值匹配某个模式,并且可以在匹配过程中绑定变量。case 还可以附带守卫:

match point:
    case (x, y) if x == y:
        print("在对角线上")
    case (x, y):
        print("普通点")

执行顺序是:

  1. 先尝试模式 (x, y)
  2. 模式成功后绑定 xy
  3. 再计算守卫 x == y
  4. 守卫为真时执行该分支;
  5. 守卫为假时继续尝试后面的 case

模式匹配成功产生的名称绑定可以在 match 语句之后继续使用;但失败匹配过程中产生的部分绑定不应被依赖,因为实现可以进行优化,失败时哪些中间绑定保留下来并不是可靠接口。(docs.python.org)

因此:

if status == "ok":
    ...

表达的是条件判断,而:

match response:
    case {"status": "ok", "data": data}:
        ...

表达的是结构匹配和绑定。两者都属于控制流,但问题模型不同。


15. 常见误解与诊断路径

误解一:else 总是“循环执行完后的代码”

错误理解:

for item in items:
    if is_match(item):
        break
else:
    handle_not_found()

这里的 else 不是无条件的收尾代码,而是“没有执行 break”时的路径。

诊断时先搜索:

  • 是否存在 break
  • 是否通过 return 离开函数;
  • 是否有异常传播;
  • for 是否其实从未执行过;
  • while 的条件是否第一次检查就为假。

误解二:break 只能退出 if

break 不关心它是否位于 if 中,它终止的是最近的循环:

while condition:
    if stop:
        break

如果需要只跳过条件分支的剩余代码,通常不应使用 break,而应使用 else、提前返回或重新组织条件。

误解三:循环变量在循环结束后自动消失

for x in [1, 2, 3]:
    pass

print(x)  # 3

普通 for 不创建新的作用域,所以 x 仍然存在。推导式的变量行为不同,不能混为一谈。

误解四:函数会优先读取全局变量

value = 10

def f():
    print(value)
    value = 20

结果不是打印 10,而是 UnboundLocalError。函数体内出现赋值后,value 被视为局部变量;问题不是运行时“找错了层级”,而是名称绑定规则在编译代码块时已经确定。

误解五:while 一定至少执行一次

while False:
    print("不会执行")

while 先判断条件,因此可能零次执行。若必须先执行一次再判断,应使用:

while True:
    do_work()

    if should_stop():
        break

误解六:修改列表后继续遍历列表是安全的

如果循环依赖容器的当前位置,删除或插入元素可能改变后续遍历行为。诊断时应记录:

  • 当前索引或元素;
  • 容器长度;
  • 修改前后的容器内容;
  • 迭代器是否仍然引用原容器。

多数情况下,遍历副本或构造新容器更容易推导。


16. 一套统一的控制流推理方法

面对复杂的 if、循环和作用域问题,可以按以下顺序分析。

第一步:标出控制流节点

把代码中的节点分为:

  • 条件判断;
  • 循环条件;
  • 迭代器取值;
  • continue
  • break
  • return
  • 异常;
  • else
  • finally

第二步:确定每个 else 的归属

根据缩进判断它属于:

  • if
  • for
  • while
  • try

不要根据视觉上“离谁最近”来判断。

第三步:为循环写出状态变量

对于 while,明确:

初始状态 → 条件 → 循环体修改 → 新状态

对于 for,明确:

可迭代对象 → 迭代器 → 当前元素 → 目标变量 → 循环体

第四步:标记每条提前离开路径

特别检查:

  • break 离开哪一层循环;
  • continue 回到哪一个循环的下一轮;
  • return 是否跳过循环 else
  • 异常是否跳过当前 else
  • finally 是否改变离开结果。

第五步:确定名称绑定位置

对每个重要名称询问:

  1. 它在哪里赋值?
  2. 赋值是否真的执行?
  3. 它属于函数局部、外层函数、模块还是内置作用域?
  4. 是否被 globalnonlocal 改写?
  5. 是否处于推导式的隐式作用域中?
  6. 是否被闭包迟绑定?

只要把“控制流路径”和“名称绑定规则”分开分析,绝大多数 UnboundLocalError、循环 else 和闭包问题都能被还原为确定的状态变化,而不是依赖直觉猜测。


系列导航与关联阅读

官方资料

本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。