本节摘要:控制流三件套——分支、循环、跳转。本节的独特任务是把 for 循环拆开:它不数数,它执行迭代协议。理解这一点后,推导式、zip、enumerate、生成器全部串成一条线。match 语句作为结构化分支的新成员也会登场。
if/elif/else 无需多讲,但两个细节值得校准。其一,Python 的条件表达式直接用对象的真假,而非必须布尔值。判定规则叫真值检验:None、False、数字 0、空容器、空字符串为假,其余为真。于是 if items: 是判断列表非空的惯用法,不必写 if len(items) > 0:。
其二,三元表达式适合"二选一赋值",但只该写一层——嵌套三元是可读性杀手:
level = "adult" if age >= 18 else "minor"
3.10 引入的 match 语句不是 C 的 switch 翻版,它是结构模式匹配——按值的形状分支:
def handle(cmd): match cmd.split(): case ["go", direction]: return f"向 {direction} 移动" case ["pick", item, *rest]: return f"拾取 {item}(额外参数 {rest})" case ["quit"]: return "再见" case _: return "未知指令" handle("pick sword gold-mode") # '拾取 sword(额外参数 ['gold-mode'])'
注意 [a, b, *rest] 的结构解构能力——switch 做不到这个。经验法则:只是比较离散值时 if/elif 足够;要按数据结构形状分发(解析指令、处理消息体)时 match 才显威力。
while 是"条件驱动",适合次数未知的循环(重试、轮询、收敛迭代):
# 牛顿迭代法开平方:循环次数事先未知 def my_sqrt(n, eps=1e-10): x = n while abs(x * x - n) > eps: x = (x + n / x) / 2 return x my_sqrt(2) # 1.4142135623746899
for 是"元素驱动",遍历任何可迭代对象。两者都支持 break(跳出)、continue(跳过本轮)、else(循环未被 break 时执行——Python 最被低估的语法):
for n in range(2, 50): for f in range(2, n): if n % f == 0: break else: # 内层没被 break:n 是素数 print(n, end=" ") # 2 3 5 7 11 ... 47
循环 else 的语义记住一句话:"找了一圈没找到"分支,搜索类代码用它免去旗标变量。
教科书写"for 遍历序列",解释器的说法更精确。for x in obj: 等价于:
_it = iter(obj) # 1. 调用 iter 拿迭代器 while True: try: x = next(_it) # 2. 反复 next 取值 except StopIteration: # 3. 停止信号,安静收场 break ... # 循环体
手动复现一遍,直觉就长出来了:
>>> it = iter("abc") >>> next(it), next(it), next(it) ('a', 'b', 'c') >>> next(it) StopIteration # Traceback 省略
两个角色要分清:可迭代对象实现 __iter__(能被 iter 接受,如 list、str、dict、range);迭代器同时实现 __next__(能被 next 驱动)。迭代器通常也是可迭代的(自己返回自己)。list 是前者不是后者:
>>> next([1, 2, 3]) TypeError: 'list' object is not an iterator >>> next(iter([1, 2, 3])) 1
一次性的消费语义也由此而来:
>>> nums = map(str, [1, 2, 3]) # map 返回迭代器 >>> list(nums), list(nums) (['1', '2', '3'], []) # 第二次已耗尽!
这个"第二次为空"的坑在 map/filter/zip/生成器上通用。对策:要么立刻物化成 list,要么只消费一次。
enumerate——要下标就别用 range(len(...)):
for i, ch in enumerate("abc", start=1): print(i, ch)
zip——并行遍历,配 strict 参数防长度不齐:
>>> names = ["ann", "bob"] >>> scores = [92, 85, 77] >>> list(zip(names, scores)) [('ann', 92), ('bob', 85)] # 静默截断,bug 温床 >>> list(zip(names, scores, strict=True)) ValueError: zip() argument 2 is longer than argument 1
推导式——循环的值收集形态。从"变形"到"过滤"再到"展平",三档语法:
squares = [x * x for x in range(10)] evens = [x for x in squares if x % 2 == 0] flat = [cell for row in matrix for cell in row] # 双层展平 upper_map = {w: w.upper() for w in ["a", "b"]} # 字典推导 seen = {c for c in "banana"} # 集合推导
我的纪律:推导式只写一层过滤 + 一层映射;需要 if/else 分支逻辑或三重循环时,老老实实写普通循环。生成器表达式(圆括号版)先按下不表,第 7 章的主角之一。
range 顺带说透:它不是 list,是惰性对象,range(10**12) 秒建零内存——因为它只存起止与步长,按下标现算。这也是迭代协议世界观的又一例证:能逐个给出的,就不必先造全。
⚠️ 常见坑三则:遍历时增删容器触发
RuntimeError(复制后再改);在循环里定义函数导致延迟绑定(下一节详解);while True忘了推进状态造成死循环。
💡 关键直觉:看到
for,脑内翻译成"iter + next + StopIteration";看到"想收集结果",先想推导式。这两条反射建立后,Python 代码的味感就正了。
顺带补一块传统上属于进阶主题的拼图。正则表达式用标准库的 re 模块接入,Python 里它与原生字符串是天然搭档——r 开头的字符串免去双重转义之苦。常用件速查:\d 匹配数字、\w 匹配字符、\s 匹配空白、点号匹配任意字符;量词加号表示一或多次、星号零或多次、问号零或一次;锚点尖号对行首、美元符对行尾。函数按用途分三档:match 从头匹配、search 全串找第一处、findall 找出全部;分组用圆括号,命名分组配 group 方法按名字取值,比数下标可读得多。两个高频工程习惯:预编译(compile 一次、复用多次,省去重复解析);能用惰性迭代器逐个取就别一次性物化成列表——又是 7.1 节的惰性思想。最后一句忠告:正则适合格式规整的文本,解析 HTML 这类嵌套结构请回到第 9 章的解析器,正则会把你带进维护地狱。
if items: 是惯用法。__iter__,迭代器给 __next__;for 是三步对话。下一节把镜头转向函数本身:它是个什么对象、名字在哪些命名空间里查找、闭包到底"闭"住了什么。