《Python编程入门》4.1 条件、循环与推导式

本节讲清 Python 的流程控制与推导式:先梳理哪些值为假,再拆解 if/elif/else、条件表达式、for 与 while、循环 else 子句、break 与 continue,以及 range、enumerate、zip(含 strict=True)。随后用列表、字典、集合、生成器四种推导式替代啰嗦循环,划清嵌套推导式边界,最后介绍 3.10 起的 match/case 模式匹配。

本节目标:掌握 Python 的分支、循环与推导式,能判断哪些值在布尔语境下为假、正确使用循环的 else 子句,并能在合适场景下用推导式与 match/case 写出更清晰的代码。
适用版本:Python 3.12+(实测 3.14.6)

4.1 条件、循环与推导式

前三章你已经认识了 Python 的数值、字符串和四种内置容器。但程序不会只做一次性计算——它要根据条件走不同的路、对一堆数据反复处理。本节把这两件事讲透,并且顺手教你 Python 里最被人称道的一个特性:推导式。

4.1.1 真值判断:哪些值是「假」的

Python 的 if 不要求条件必须是 bool,任何对象都能放进条件里,解释器会调用它的真值。下面这些是内置的假值(falsy):

values = [0, 0.0, "", [], {}, set(), None, False, 0j]
for v in values:
    print(repr(v), "->", bool(v))

实测输出:

0 -> False
0.0 -> False
'' -> False
[] -> False
{} -> False
set() -> False
None -> False
False -> False
0j -> False

规律很好记:数字看是否为 0,容器看是否为空,None 与 False 天然为假。 反过来,只要不在这张表里,几乎都是真值——连字符串 "0"、只含一个 0 的列表 [0] 都是真。

这条规则带来的最大好处是可以写出符合直觉的判断:if not items: 同时覆盖了「items 是空列表」和「items 是 None」两种情况。但要小心一个经典坑——用真值判断来区分 0 和「没传值」会出错,因为 0 本身就是假值,这种场景应显式写 if x is None。

4.1.2 if / elif / else 与条件表达式

分支结构本身没有意外,Python 用缩进代替花括号:

def check(age):
    if age < 0:
        return "非法"
    elif age < 18:
        return "未成年"
    else:
        return "成年"

print(check(20), check(10))

实测输出 成年 未成年。分支是从上往下第一个命中即停,所以 elif 的顺序会影响结果,范围判断务必把更具体的条件放在前面。

Python 还提供了条件表达式(也叫三元表达式),把简单的 if/else 压成一行:

age = 20
label = "成年" if age >= 18 else "未成年"
print(label)

实测输出 成年。它也可以链式书写(形如 "A" if s >= 90 else "B" if s >= 80 else "C"),但超过两层就该改回 if/elif/else。条件表达式是表达式而非语句,因此能直接放进赋值、函数实参、推导式里——这正是它的价值所在。

4.1.3 for 与 while

for 在 Python 里是迭代器循环,而不是 C 风格的计数器循环。它遍历任何可迭代对象:

for ch in "abc":
    print(ch, end=" ")
print()
for key in {"a": 1, "b": 2}:
    print(key, end=" ")
print()

实测输出 a b c 和 a b。注意遍历字典默认拿到的是键,要键值对得用 .items()。

while 则用于「条件为真就一直做」的场景,适合循环次数事先未知的情况:

n = 27
steps = 0
while n != 1:
    n = n // 2 if n % 2 == 0 else 3 * n + 1
    steps += 1
print("步数 =", steps)

实测输出 步数 = 111(这是著名的 Collatz 序列)。选择原则很简单:能数得清次数用 for,靠条件判断是否继续用 while。

4.1.4 循环的 else 子句

Python 的 for/while 可以带一个 else,它在循环正常结束(没有 break)时执行:

def find_even(nums):
    for n in nums:
        if n % 2 == 0:
            print("找到偶数", n)
            break
    else:
        print("没有偶数")

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

实测输出 找到偶数 4 与 没有偶数。

第一次调用因为 break 提前退出,else 被跳过;第二次循环跑完也没命中,else 执行。它等价于「设一个 found 标志位」,但省掉了一个变量。很多语言没有这个特性,读代码时如果见到 else 缩进和 for 对齐,记住它的语义是「没被 break 打断」而不是「条件不成立」。

4.1.5 break 与 continue

  • break:立即结束整个循环,跳到循环之后。
  • continue:结束本次迭代,直接进入下一次。
total = 0
for n in range(1, 11):
    if n % 2 == 0:
        continue      # 偶数跳过
    if n > 8:
        break         # 超过 8 就停
    total += n
print("total =", total)

实测输出 total = 16(累加了 1、3、5、7)。continue 只是「跳过本轮剩余语句」,break 才是彻底收工。注意 break 只能跳出最内层一层循环,想一次跳出多层得用函数封装配合 return,或抛出异常。

4.1.6 range 的语义

range 不是列表,而是一个惰性的、不可变的整数序列,只在迭代时按需生成:

print(list(range(5)))        # [0, 1, 2, 3, 4]
print(list(range(2, 10, 3))) # [2, 5, 8]
print(list(range(5, 0, -1))) # [5, 4, 3, 2, 1]
print(list(range(0)))        # []

三条规律:range(stop) 从 0 开始、不含 stop;range(start, stop, step) 支持步长,负步长可倒序;当 start >= stop 且步长为正时结果为空。range 支持 len() 和下标访问,但不生成全部元素,因此 range(10**9) 也只占几十字节内存。

4.1.7 enumerate 与 zip

需要「同时拿到下标和元素」时,别手写 i = 0 自增,用 enumerate:

names = ["Ada", "Alan", "Grace"]
scores = [98, 91, 95]
for i, (name, score) in enumerate(zip(names, scores), start=1):
    print(i, name, score)

实测输出 1 Ada 98、2 Alan 91、3 Grace 95。

这里同时用到了 zip——它把多个可迭代对象按位置打包成元组。默认情况下 zip 会在最短的那个耗尽时停下:

print(list(zip([1, 2, 3], ["a", "b"])))  # [(1, 'a'), (2, 'b')]

这种静默截断经常掩盖数据长度不匹配的 bug。Python 3.10 起 zip 支持 strict=True,长度不一致时直接报错:

try:
    list(zip([1, 2, 3], ["a", "b"], strict=True))
except ValueError as e:
    print("ValueError:", e)

实测输出 ValueError: zip() argument 2 is shorter than argument 1。只要两个序列本应等长,就加上 strict=True——它把「悄悄少处理几条」变成「当场失败」。

4.1.8 四种推导式

推导式(comprehension)是用一行表达式从可迭代对象构造新容器的语法。四种形态如下:

words = ["Hello", "world", "PYTHON", "code"]
print([w.lower() for w in words if len(w) > 4])   # 列表推导式
print({w: len(w) for w in words})                 # 字典推导式
print({w[0].lower() for w in words})              # 集合推导式
gen = (len(w) for w in words)                     # 生成器表达式
print(type(gen).__name__, sum(gen))

实测输出依次为 ['hello', 'world', 'python']、{'Hello': 5, 'world': 5, 'PYTHON': 6, 'code': 4}、{'p', 'h', 'w', 'c'} 与 generator 20。

形态语法结果类型
列表推导式[expr for x in it if cond]list
字典推导式{k: v for x in it}dict
集合推导式{expr for x in it}set
生成器表达式(expr for x in it)generator

四种里最需要留意的是生成器表达式:它用圆括号,不立即求值,只在被消费时逐个产出。上例 sum(gen) 之所以得到 20,是因为生成器把四个单词的长度边算边加,全程没有建出中间列表。处理大数据集时这一点能省下可观内存,第 8 章会展开生成器协议。

4.1.9 嵌套推导式与可读性边界

推导式可以嵌套,多层 for 的顺序与普通循环一致——写在前面的在外层:

matrix = [[1, 2, 3], [4, 5, 6]]
print([n for row in matrix for n in row])       # 拍平
print([[n * 2 for n in row] for row in matrix]) # 逐行变换

实测输出 [1, 2, 3, 4, 5, 6] 与 [[2, 4, 6], [8, 10, 12]]。

第一行是展平(外层遍历行、内层遍历列),第二行是逐行映射。两者的区别只在于 for 的位置,很容易写错,可读性也随嵌套层数急剧下降。经验边界是:

  • 一层 for + 至多一个 if:放心用。
  • 两层 for 且只做展平或简单映射:可以,但加一行注释。
  • 出现多个 if、if/else 三元、或三层以上嵌套:改回显式 for 循环。

推导式是工具不是信仰。一段需要读三遍才懂的推导式,不如一段五行的循环。

4.1.10 for 循环还是推导式

选择标准不是「哪个更短」,而是「结果是要用一次,还是要留下来」:

场景推荐理由
要构造一个新的 list/dict/set推导式意图明确、通常更快
要消费大量数据但只算一个汇总值生成器表达式惰性、省内存
循环体有多个语句或副作用(打印、写文件)for 循环推导式只适合纯表达式
需要 break/continue 精细控制for 循环推导式不支持提前退出

一句话:推导式表达「构造」,循环表达「过程」。 当你在推导式里塞进副作用或复杂逻辑时,说明你真正需要的是循环。

4.1.11 match / case 结构化模式匹配

Python 3.10 起,语言加入了 match/case。它不是其他语言里的 switch——switch 比较的是值,而 match 匹配的是结构,并能把结构里的片段绑定到变量:

def handle(command):
    match command.split():
        case ["go", direction]:
            return f"向 {direction} 移动"
        case ["go", direction, "fast"]:
            return f"向 {direction} 快速移动"
        case ["look"] | ["look", "around"]:
            return "环顾四周"
        case ["take", item, *rest]:
            return f"拾取 {item},附带 {rest}"
        case {"action": "quit"}:
            return "退出"
        case _:
            return "无法识别"

for cmd in ["go north", "look", "take sword shield", "x"]:
    print(handle(cmd))

实测输出依次为 向 north 移动、环顾四周、拾取 sword,附带 ['shield'] 与 无法识别。

这段演示了四种模式:序列模式(["go", direction] 按位置解构列表)、或模式(| 任一命中即可)、星号模式(*rest 收集剩余元素)、映射模式({"action": "quit"} 按键匹配)。case _ 是兜底通配符。

模式还能匹配类,配合守卫子句(if)进一步收窄:

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def classify(p):
    match p:
        case Point(x=0, y=0):
            return "原点"
        case Point(x=0, y=y) if y > 0:
            return f"y 轴正半轴 y={y}"
        case Point(x=x, y=0):
            return f"x 轴上 x={x}"
        case Point():
            return "其他点"
        case _:
            return "非点对象"

print(classify(Point(0, 0)), classify(Point(0, 5)))
print(classify(Point(3, 0)), classify(Point(2, 3)), classify("hello"))

实测输出 原点 y 轴正半轴 y=5 与 x 轴上 x=3 其他点 非点对象。

要记住几点与 switch 的本质差异:

  • match 自上而下尝试,第一个成功的 case 即生效,不会「穿透」。
  • 模式里的裸名字是绑定,不是比较——case x 会匹配任何值并把值绑定给 x,想比较常量要用点号名字(如 case Color.RED)或字面量。
  • 变量只在命中的那个分支里存在,分支之间互不干扰。

match/case 适合替代那种「先取类型再层层 isinstance」的分派代码,尤其配合数据类时非常清晰。但它不是必须的——普通 if/elif 完全能表达同样的逻辑,选择看哪种更能表达你的意图。

小结

  • 真值判断遵循「数字看是否为零、容器看是否为空」,None/False 为假;判断「是否传值」时要用 is None 而不是真值。
  • for 是迭代器循环,while 靠条件;range 是惰性序列且不含终点。
  • 循环的 else 在未被 break 时执行;break 只跳出最内层。
  • enumerate 同时取下标与元素,zip 按位置打包——两序列本应等长时用 strict=True(3.10 起)暴露长度不匹配。
  • 推导式有列表/字典/集合/生成器四种形态;嵌套超过两层或含副作用时应改回 for 循环。
  • match/case(3.10 起)是结构化模式匹配而非值比较,支持序列、映射、类模式与守卫子句。

下一节我们把这些控制结构收进函数:你会看到参数的五种形态、默认值的求值时机陷阱,以及 global/nonlocal 背后的 LEGB 作用域规则——它们决定了上面这些循环与分支代码如何被组织成可复用的单元。

阅读导航:上一节:3.3 可变与不可变、引用语义与拷贝 · 下一节:4.2 函数:参数传递、默认值与作用域 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时