编程语言研究书库

循环与 if — 控制流为什么是最易出错的地方

循环与 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,然后升级为循环版:

for _ in range(3):
    print("hello", end=" ")
# hello hello 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):

for i in range(3):
    for j in range(3):
        print(i, j)   # 0 0 / 0 1 / …

作者的预算很硬:「三维物体,三层循环可以接受;但别再往里加逻辑——那已经够难懂了。」 层数对应维度,不对应业务复杂度;业务逻辑塞进嵌套循环,就是复杂度相乘。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 跟踪不了这么多。

(2³=8 这个数是为演示编的,书里没算;层数与组合数的对应关系是通用知识。)

替代手法:把内层条件用 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

  1. 循环和 if 让程序分叉重复,但人脑跟踪不了复杂控制流——所以控制流越少越好。
  2. for x in xs 优于一切下标写法;空列表天然安全。
  3. if 是高危行:能平铺不嵌套;「需要想一想才明白」的逻辑就该重写。
  4. 消灭 if 的三扇门: 多态(第 11/12 章)、for-in、依赖注入(第 15 章)。
  5. 嵌套循环的预算 = 维度数;业务逻辑别进嵌套循环。
  6. and/or 对称、空即 False;布尔太多是复杂信号。
  7. 循环里不用提前 return(搜索语义除外,见判断块);break/continue 用了先反思。

8A. 控制流的替代升级路线

把本章「消灭 if」的三扇门按升级顺序排,实战里按序尝试:

  第一扇门:for x in xs(零成本,先开)
  第二扇门:多态 / 函数对象(要动数据结构,中等成本)
  第三扇门:依赖注入(要动接口,最高成本,回报也最高——第 15 章详述)
  图说:门的成本递增,但每扇门打开后,后面的门会更容易开;
       跳过第一扇直接开第三扇,等于在满是 if 的代码上做 DI,事倍功半。

(这张路线图是我们的归纳,书里只分别给了三扇门,没排顺序。)

9. 原文地图

主题原书章原文位置
本章定位:分叉与重复4 Loops and If Statementstext/20-ch04-4-loops-and-if-statements.txt:4(搜「branch and repeat」)
「最好的程序员是懒的」4 Loops and If Statementstext/20-ch04-4-loops-and-if-statements.txt:9(搜「lazy」)
_ 占位约定4 Loops and If Statementstext/22-ch06-6-functions-and-methods.txt:67(搜「placeholder」)
构造偶数列表、推导式对照4 Loops and If Statementstext/20-ch04-4-loops-and-if-statements.txt:28(搜「even_numbers」)
嵌套循环的预算4 Loops and If Statementstext/20-ch04-4-loops-and-if-statements.txt:65(搜「Nested Loops」)
if 高危行的论证4 Loops and If Statementstext/20-ch04-4-loops-and-if-statements.txt:99(搜「likely to contain an error」)
消灭 if 的门(多态、for-in、空列表)4 Loops and If Statementstext/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 Statementstext/20-ch04-4-loops-and-if-statements.txt:138(搜「In theory」)
布尔逻辑与真值表、对称性4 Loops and If Statementstext/20-ch04-4-loops-and-if-statements.txt:149(搜「truth table」) · text/13-fm-lists.txt:23(搜「symmetric」)
隐式转换、bool 示例4 Loops and If Statementstext/20-ch04-4-loops-and-if-statements.txt:162(搜「bool(0)」)
return/break/continue 的裁决4 Loops and If Statementstext/20-ch04-4-loops-and-if-statements.txt:186(搜「three important concepts」) · text/20-ch04-4-loops-and-if-statements.txt:207(搜「early returns」)

Footnotes

  1. 出处:「4 Loops and If Statements」第 3–5 段(text/20-ch04-4-loops-and-if-statements.txt:4,搜「branch and repeat」)。 ↩

  2. 出处:「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」)。 ↩ ↩2

  3. 出处:「4 Loops and If Statements」第 55–63 段(text/20-ch04-4-loops-and-if-statements.txt:56,搜「common technique」)。 ↩ ↩2

  4. 出处:「4 Loops and If Statements」第 67–79 段(text/20-ch04-4-loops-and-if-statements.txt:77,搜「hard to understand」;第 78 段,搜「three-dimensional」)。 ↩

  5. 出处:「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」)。 ↩

  6. 出处:「4 Loops and If Statements」第 105–120 段(text/20-ch04-4-loops-and-if-statements.txt:107,搜「polymorphism」;第 118–120 段,搜「empty lists」)。 ↩ ↩2

  7. 出处:「4 Loops and If Statements」第 137–143 段(「Nested If Statements」框,text/20-ch04-4-loops-and-if-statements.txt:138,搜「In theory」)。 ↩

  8. 出处:「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」)。 ↩ ↩2

  9. 出处:「4 Loops and If Statements」第 160–175 段(text/20-ch04-4-loops-and-if-statements.txt:161,搜「converted to False」)。 ↩

  10. 出处:「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」)。 ↩ ↩2

On this page