循环与 if — 控制流为什么是最易出错的地方¶
这一章讲让程序「拐弯和重复」的两件工具:循环与 if。 但原书的讲法不像语法教材: 它先承认这两件工具必不可少,然后立刻开始论证它们有多危险—— 「含 if 语句的行,比不含 if 的行更容易含错误」——并给出了一整套「能不用就不用」的替代方案。 读完你会理解作者那句口号:最好的代码,是没有 if 的代码。
1. 这一章讲什么¶
原书开宗明义:循环和 if 让代码不再是从上到下直行,而能分叉和重复—— 这是软件开发里最重要的两个概念之一。1
但本章真正的主题是风险。作者的观点一句话:软件开发的瓶颈是人,不是计算机; 人理解不了复杂的控制流,所以代码必须写成「让人少犯错」的形状——具体说, if 尽量少、嵌套尽量浅、提前退出尽量没有。
工具 作者的态度
for 循环 好,但用 for x in xs 的下标版是历史遗物
嵌套循环 三维物体三层循环到头,再多逻辑就该拆
if 语句 有用但危险;用多态和 for-in 替掉大部分
提前 return if 里可用,循环里禁用(破坏「一视同仁」)
break/continue 能不用就不用,用了先反思结构
图说:这一章的每一节都是「工具 + 它的风险 + 替代手法」的三段式。
2. 核心原理一:循环¶
2.1 for 循环与 range¶
解决什么问题: 同一件事要做好几遍——最好的程序员是「懒」的:要写三遍的东西,他们写一遍,交给循环。
怎么做(主线例子): 打印三行 hello,然后升级为循环版:
走一遍:range(3) 依次给出 0、1、2;for 把每个值放进循环变量;循环体执行三遍。
end=" " 是告诉 print「结尾别换行,改成空格」;循环变量用下划线 _,
这是 Python 的约定:语法上必须要个变量、但你用不到它时,就叫 _。2
循环变量有用起来的版本——构造偶数表:
even_numbers = []
for i in range(5):
even_numbers.append(2*i)
# [0, 2, 4, 6, 8]
# 或者一行:even_numbers = [2*i for i in range(5)] ← 第 04 章的列表推导式
遍历列表是 for 的主战场(第 04 章已详讲):for fruit in ["apple", "pear", "banana"]——
书里补了一句历史注脚:老式语言(C++)里你必须亲手管理下标,「容易差一位」;
现在连 C++ 都支持 for-in 写法了,「那些日子过去了」。3
2.2 嵌套循环:层数是硬预算¶
循环里套循环,最常见的正当场景是遍历二维结构(矩阵、棋盘——第 19 章的渗流模拟就是三层 for):
作者的预算很硬:「三维物体,三层循环可以接受;但别再往里加逻辑——那已经够难懂了。」 层数对应维度,不对应业务复杂度;业务逻辑塞进嵌套循环,就是复杂度相乘。4
3. 核心原理二:if 语句——祝福与诅咒¶
3.1 基本形状与「if 是高危行」¶
怎么做: 分支的基本形(书里的例子——超速警告):
def speed_limit(v):
if v > 60:
print("You're driving too fast!")
speed_limit(61) # You're driving too fast!
为什么危险: 书里把这个判断放在了加框的位置:「含 if 语句的行,比不含 if 的行出错概率高得多—— 原因很简单:人处理这种逻辑很吃力。既然软件开发主要是关于人而不是计算机, 我们就必须写出让人犯最少错的代码——也就是含尽可能少 if 的代码。」5
这句论证值得看清结构:错误不来自机器(机器执行 if 从不出错),来自读者的理解失败—— 又回到第 02 章的地基:读多于写。
3.2 消灭 if 的几种手法¶
书里给了三扇门(他的原话:「我只能给你指门,走过去得靠你自己」):
① 多态(polymorphism)替代类型判断。 与其 if type == car: ... elif type == spaceship: ...,
不如让两种对象实现同一个方法,调用处不分类型——for vehicle in vehicles: vehicle.move()。
这是第 11、12 章类的主线,这里先记名字:多态是消灭 if 的正规军。6
② for-in 替代「古典 for + 守卫 if」。 老写法要检查下标是否越界、元素是否符合条件;
for car in cars: 把这些全部消掉——包括空列表的特例:遍历空列表,什么都不发生,
而这通常正是你要的行为;老式写法你得先写个 if 判空——一个常见 bug 源就这样免费消失了。6
③ 依赖注入替代「真数据/假数据」开关。 if use_real_data: 这类运行时开关是第 15 章要拆掉的靶子。
3.3 嵌套 if:理论上没错,实践上禁止¶
书里对嵌套 if 的评价是本章最硬的一句:「理论上,嵌套 if 没有任何问题——计算机执行它们毫不费力, 人也可能写对。但我们不是在理论上写代码。代码由人写、给人读,而人在 if 上犯的错特别多。 要避免 if,就一定要避免嵌套 if——它们太容易出错了。」7
if v > 60: ← 第 1 层
if is_drunk: ← 第 2 层:读者要同时跟踪两个条件
print("And you're also drunk!")
图说:每多一层,读者脑子里要同时成立的假设就多一个;
三个条件就是 2³=8 种组合——人脑 reliably 跟踪不了这么多。
替代手法:把内层条件用 and 平铺(if v > 60 and is_drunk:),或干脆拆函数。
4. 核心原理三:布尔代数与隐式转换¶
4.1 真值表与三个运算符¶
解决什么问题: 条件复杂起来,要靠 and/or/not 组合;组合对不对,要靠真值表(truth table,枚举所有输入组合与结果的表)兜底。
书里把八种组合逐一列了(not True→False、True and False→False、True or False→True……),
值得自己敲一遍。两条性质可以顺手记住:and 和 or 都对称——True and False 与 False and True 同值。8
4.2 隐式转换:空即 False¶
Python 会把值自动转成布尔:0、空串、空列表是 False,其余是 True(bool([0]) 是 True——[0] 不是空列表)。
于是 if x: 是「x 非空非零」的简写。9
作者照例给了风险注:「逻辑运算符也很容易出错;如果你需要想一想才知道这行代码干什么,就该重写。」 而且布尔条件太多本身就是信号——「过多的布尔使用说明代码太复杂,该简化」。
5. 核心原理四:return、break、continue——提前退出是坏味道¶
三个词,一个机制:在到达块末尾之前离开。
| 词 | 离开什么 | 带不带值 | 作者的用法裁决 |
|---|---|---|---|
return |
整个函数 | 带 | if 里提前 return 尚可接受;循环里禁用 |
break |
整个循环 | 不带 | 尽量少 |
continue |
本轮循环,进下一轮 | 不带 | 尽量少 |
循环里提前 return 为什么禁用?回到第 04 章的哲学:列表的所有元素应该被一视同仁地对待。 循环体执行到一半就 return,说明后面的元素没被同等对待——那是数据结构或函数切分出了问题的信号。10
for i in range(5):
if i == 3:
continue # 跳过 3 → 打印 0 1 2 4
print(i)
# 作者问:为什么不能写成没有 continue 的版本?多数时候能。
书里的总结句:「如果你用了提前 return、break、continue,先问自己能不能写得更好——多数时候,能。」10
三个词的差别再压实一遍,这张表值得放进肌肉记忆:
| 词 | 离开什么 | 带走什么 | 典型误用 |
|---|---|---|---|
return |
整个函数 | 一个值 | 在循环里提前 return(搜索以外) |
break |
整个循环 | 不带值 | 用 break 当「函数没找着」的信号 |
continue |
本轮迭代 | 不带值 | 条件反了还 continue,逻辑拧成麻花 |
「误用」列的共同根源:三者都在改变代码的阅读顺序,而读者的默认假设是顺序执行。 每用一次,就要替读者在脑子里记一条例外——例外多了,代码就读不动了——读不动,就是本章开头那句「软件开发主要是关于人」的微观样子——人读得顺,机器自然跑得顺,顺序别搞反;机器从不出错,出错的都是读代码的人——以及写代码的人,两者多半是同一个——三个月后的自己,是团队里最陌生的读者——对他好一点,就是对自己好一点——这是本章最不技术、也最实在的一条建议。
6. 作者的判断与证据¶
书里给了证据的: 所有控制流示例可直接运行;「空列表遍历什么都不发生」是可验证的机制事实; 真值表可逐一核对。38
作者的判断(本章占比最高):
- 「含 if 的行更易错」「嵌套 if 应避免」——立场性主张,依据是「人处理逻辑的能力有限」,合理但不可证明;
- 「最好的代码没有 if」——口号式表达,实际含义是「if 的数量是复杂度的温度计」;
- 「提前退出是坏味道」——与主流「卫语句(guard clause)提前 return」风格相反;主流风格认为函数开头的参数校验提前 return 反而扁平化。这是本书与常见建议冲突最明显的一处。
判断(我们的,不是书里的): 本章的「if 最少化」方向正确,但「循环里提前 return 禁用」我们建议打折: 在纯函数里,找到答案立刻 return 往往比「设标志位、跑完全程再判断」更可读—— 作者真正想守的是「所有元素一视同仁」,找首个匹配项这类「搜索」语义天然不是一视同仁,强行统一反而绕。 如果错,会错在: 当循环体携带累积状态(求和、收集)时,中途 return 确实会留下「半完成的累积」这种理解负担—— 那种场景作者的禁令是对的。判据:循环是「搜索」还是「变换/累积」。
7. 边界与局限¶
- 没讲
while循环——原书第 19 章的渗流模拟用到了它(while still_burning),但本章没教; - 没讲条件表达式(
x if cond else y); - 真值表只给了三个运算符的,复合表达式的化简(德摩根律等)没提;
- break 的正当日场景(如「找到即停」的线性搜索——从头一个个查到尾)被一刀切了,见我们的判断块;
while循环在本章完全没出现(原书把它推迟到第 19 章渗流模拟的现场教学)——补一句最小语义:while 条件:先检查条件、条件真就执行体,适合「不知道要循环几次、只知道停止条件」的场景;
8. 可带走的¶
本章主线例子,一行写完: for _ in range(3): print("hello", end=" ") →
循环变量用不上的约定叫 _;for x in xs 遍历空列表自然安全——少写一个 if 判空。2
- 循环和 if 让程序分叉重复,但人脑跟踪不了复杂控制流——所以控制流越少越好。
for x in xs优于一切下标写法;空列表天然安全。- if 是高危行:能平铺不嵌套;「需要想一想才明白」的逻辑就该重写。
- 消灭 if 的三扇门: 多态(第 11/12 章)、for-in、依赖注入(第 15 章)。
- 嵌套循环的预算 = 维度数;业务逻辑别进嵌套循环。
- and/or 对称、空即 False;布尔太多是复杂信号。
- 循环里不用提前 return(搜索语义除外,见判断块);break/continue 用了先反思。
8A. 控制流的替代升级路线¶
把本章「消灭 if」的三扇门按升级顺序排,实战里按序尝试:
第一扇门:for x in xs(零成本,先开)
第二扇门:多态 / 函数对象(要动数据结构,中等成本)
第三扇门:依赖注入(要动接口,最高成本,回报也最高——第 15 章详述)
图说:门的成本递增,但每扇门打开后,后面的门会更容易开;
跳过第一扇直接开第三扇,等于在满是 if 的代码上做 DI,事倍功半。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 本章定位:分叉与重复 | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:4(搜「branch and repeat」) |
| 「最好的程序员是懒的」 | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:9(搜「lazy」) |
_ 占位约定 |
4 Loops and If Statements | text/22-ch06-6-functions-and-methods.txt:67(搜「placeholder」) |
| 构造偶数列表、推导式对照 | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:28(搜「even_numbers」) |
| 嵌套循环的预算 | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:65(搜「Nested Loops」) |
| if 高危行的论证 | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:99(搜「likely to contain an error」) |
| 消灭 if 的门(多态、for-in、空列表) | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:107(搜「polymorphism」) · text/20-ch04-4-loops-and-if-statements.txt:118(搜「empty lists」) |
| 嵌套 if:理论上没错 | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:138(搜「In theory」) |
| 布尔逻辑与真值表、对称性 | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:149(搜「truth table」) · text/13-fm-lists.txt:23(搜「symmetric」) |
| 隐式转换、bool 示例 | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:162(搜「bool(0)」) |
| return/break/continue 的裁决 | 4 Loops and If Statements | text/20-ch04-4-loops-and-if-statements.txt:186(搜「three important concepts」) · text/20-ch04-4-loops-and-if-statements.txt:207(搜「early returns」) |
-
出处:「4 Loops and If Statements」第 3–5 段(
text/20-ch04-4-loops-and-if-statements.txt:4,搜「branch and repeat」)。 ↩ -
出处:「4 Loops and If Statements」第 9–26 段(
text/20-ch04-4-loops-and-if-statements.txt:18,搜「for _ in range(3)」)。下划线约定见「6 Functions and Methods」第 64–68 段(text/22-ch06-6-functions-and-methods.txt:67,搜「placeholder」)。 ↩↩ -
出处:「4 Loops and If Statements」第 55–63 段(
text/20-ch04-4-loops-and-if-statements.txt:56,搜「common technique」)。 ↩↩ -
出处:「4 Loops and If Statements」第 67–79 段(
text/20-ch04-4-loops-and-if-statements.txt:77,搜「hard to understand」;第 78 段,搜「three-dimensional」)。 ↩ -
出处:「4 Loops and If Statements」第 97–103 段(「The Danger of If Statements」框,
text/20-ch04-4-loops-and-if-statements.txt:99,搜「likely to contain an error」)。 ↩ -
出处:「4 Loops and If Statements」第 105–120 段(
text/20-ch04-4-loops-and-if-statements.txt:107,搜「polymorphism」;第 118–120 段,搜「empty lists」)。 ↩↩ -
出处:「4 Loops and If Statements」第 137–143 段(「Nested If Statements」框,
text/20-ch04-4-loops-and-if-statements.txt:138,搜「In theory」)。 ↩ -
出处:「4 Loops and If Statements」第 145–158 段(
text/20-ch04-4-loops-and-if-statements.txt:149,搜「truth table」)。对称性见「Lists」第 23–24 段(text/13-fm-lists.txt:23,搜「symmetric」)。 ↩↩ -
出处:「4 Loops and If Statements」第 160–175 段(
text/20-ch04-4-loops-and-if-statements.txt:161,搜「converted to False」)。 ↩ -
出处:「4 Loops and If Statements」第 184–193 段与第 207–231 段(
text/20-ch04-4-loops-and-if-statements.txt:189,搜「a sign of bad」;第 207 段,搜「early returns」;「在大多数情况下,你可以」见第 191 段,搜「In most cases」)。 ↩↩