← 返回知识卡片总目录

DS1号 · 自学教材系列 | 编程

第 7 章 流程控制:条件与分支

到目前为止,程序都是「一条直线走到底」。但真实世界充满「如果……就……」。这一章,程序终于学会做选择:条件满足走一条路,不满足走另一条。更重要的是,我们会扒开 if/else 的外衣,看到它在硬件层面不过是一次「跳转」。

🎯 本章学习目标

  1. 理解程序为什么需要「条件分支」,以及它让程序「聪明」的本质。
  2. 掌握 if、else、elif 的写法和执行规则。
  3. 理解条件分支的底层原理:它其实是 CPU 的「跳转指令」。
  4. 理解 Python 用「缩进」划分代码块,以及缩进错乱的后果。

7.1程序为什么需要「选择」

回顾前几章的程序:它们都是死板的直线——从上到下,一句句执行,没有岔路。但现实的问题几乎都需要「看情况」:

这种「根据条件决定走哪条路」的能力,叫 流程控制(control flow)里的条件分支(branching)。它让程序从「只会照着做」进化成「会看情况做」——这是程序「智能感」的第一个来源。

核心思想 程序之所以「聪明」,是因为它能根据数据做出不同的反应。而「做出不同反应」的最小单元,就是条件分支:判断一个条件是真是假,然后走对应的路。没有分支,程序永远只能做一模一样的事。

深入一步:程序世界只有三种「走法」

把视角拉高一点:无论多大的软件,代码的走向其实只有三种基本形态——顺序(一句接一句往下走)、分支(在岔路口挑一条走)、循环(绕圈重复)。这不是经验之谈,而是被严格证明过的结论:1966 年,计算机科学家 Böhm 与 Jacopini 证明了「任何可以写出来的程序,都能只用顺序、分支、循环这三种结构组合出来」,它后来被称为结构化程序设计的基石定理。本教材安排的章节次序,正是沿着这条定理铺开的:顺序结构在前面几章不知不觉地用熟了,本章补上分支,下一章补上循环,三块拼图凑齐,你就有了写出任何程序的全部积木。

三种结构里,顺序是 CPU 的默认状态——它生来就会「取一条指令、执行、再取下一条」;分支和循环则都要靠上一章说的「跳转指令」来实现。换句话说:程序的全部「活力」,都来自对默认顺序流的打断。值得再想一层的问题:分支到底「智能」在哪里?其实一点都不神秘——程序并不「理解」条件,它只是把条件算成一个布尔值,然后按真值机械地选路。所谓「程序会判断」,本质是「数据决定路径」。分支的价值不在聪明,而在于让同一段代码面对不同数据时表现出不同行为——这一件事就足以支撑起检查、告警、放行、计费等一切「看情况」的业务规则。

📐 例题 1 把一句检查条文翻译成 if

维护手册里有这样一句条文:「若刹车温度超过 300°C,则必须等待其降至 250°C 以下,方可继续后续检查。」请把它翻译成 Python 的 if 语句,设变量 temp 中存着当前刹车温度(°C),当前读数为 318。

思路 「条文 → 代码」是工程里最常见的翻译动作。把句子拆成两半:条件部分是「温度超过 300」,写成布尔表达式 temp > 300;动作部分是「提示等待降温」,先用 print 落实。注意「超过」是严格大于——恰好等于 300°C 时并不算「超过」,边界归谁,要以条文原文为准。

解 代码如下:

temp = 318
if temp > 300:
    print("刹车温度超限,请等待降至 250°C 以下再继续")
print("检查流程继续")

验证 temp = 318 时,318 > 300 为 True,两行都会打印;若 temp = 280,条件为 False,第一行整块被跳过,只打印「检查流程继续」。这种「触发告警但流程继续」的提示类逻辑,最适合用不带 else 的单分支 if(机器实测输出:警告行 + 检查流程继续)。

答:条件是 temp > 300,动作为打印等待提示,如上。⚠ 易错点:把「超过 300」写成 temp >= 300——差一个等号就是另一条业务规则;翻译条文时,每个边界符号都要回原文对质。

工程视角 真实系统里这类「条文式判断」无处不在:油量低于标准就告警、重量超限就拒绝放行、舱门未关闭就不能起飞。工程师读手册的基本功之一,就是把「若……则……」逐句精确地翻译成布尔条件——翻译的精度,就是程序的精度:条文差一个「等号」,代码就必须差一个等号。

辨析:分支与循环是仅有的两种「打断」

顺序执行的程序像笔直的公路;分支是岔路口(选一条,不回头),循环是环岛(绕回起点再来一圈)。两者在底层都靠「改写程序计数器」实现,区别只是方向:分支往前跳,循环往回跳。后面每一章——函数、列表、文件——都建立在这三种结构的组合之上,所以这一章值得慢下来,把每个细节真正吃透。

再进半步:不要发明「第四种走法」

结构定理的另一半含义常被忽略:既然三种结构已经「足够」,就不必也不该发明第四种走法。初学者最常发明的第四种走法,是用复制粘贴一百行来代替循环,或用一堆长得一样的独立 if 来代替 elif 链——程序照样能跑,但你正在放弃语言已经递到手上的最佳工具,并且给未来埋下一堆「改一处漏一处」的隐患。读代码时识别「这里其实在重复」、写代码前自问「这该用哪种结构」,本身就是程序设计直觉的第一课:结构选对,代码就短一半。

7.2if:条件为真才执行

最基础的分支是 if 语句。它的结构是:

if 条件:
    要执行的代码   # 注意前面有缩进

它的含义是:如果「条件」为 True,就执行下面缩进的代码;如果为 False,就跳过它,什么也不做。

看一个完整例子,并逐行解释:

age = 20
if age >= 18:
    print("你是成年人了")
print("程序结束")
  1. age = 20:把 20 存进变量 age。
  2. if age >= 18::计算 age >= 18,即 20 ≥ 18,结果是 True。
  3. 因为条件为 True,执行缩进的 print("你是成年人了"),屏幕打印这句话。
  4. print("程序结束"):这条没有缩进,不属于 if,无论条件真假都会执行。
if 语句的流程图 流程图:开始,判断条件,条件为真执行代码块,否则跳过,最后汇聚到结束。 开始 age ≥ 18 ? 打印"成年了" (条件为真) 跳过这段 (条件为假) 结束 是 否
图 7-1if 语句的流程图:先判断条件,为真执行代码块,为假跳过,最后两条路都汇聚到「结束」。

🧠 一句话记忆 if 条件: 就是「如果条件为真,就做下面缩进的事;否则跳过」。冒号 + 缩进,是 Python 分支的标志。

深入一步:条件位置上的「真值测试」

if 冒号前面的那个「条件」,在 Python 里要经历一个严谨的求值过程:先把表达式的值算出来,再做一次「真值测试」。比较运算 age >= 18 的结果是一个货真价实的布尔值(True 或 False),if 拿到它直接定夺。但 Python 的宽容在于:即使条件位置放的不是布尔值,它也能测——数字 0、空字符串 "" 会被判为「假」,其余几乎一切(非零数字、非空字符串)都是「真」。这个机制叫真值测试(truthiness)。

count = 0
if count:              # 0 被判为假,这行不打印
    print("有剩余")
if "保养记录":           # 非空字符串,判为真
    print("记录存在")

(机器实测:只打印「记录存在」。)真值测试让代码可以写得像自然语言——「如果有记录就……」「如果还有剩余就……」。不过对初学者,更稳妥的习惯是把条件写成明确的布尔表达式(count > 0、records != ""),让意图一目了然;简写留给熟练以后再享受。

还有一个值得停一秒的事实:True 和 False 本身也是值,可以存进变量、可以互相比较。判断条件的本质,是程序在运行的那个瞬间,把一串数据「归约」成一个布尔值,再交给 if 去选路。第 6 章学的运算符优先级,决定了这串归约的先后次序——这也是复杂条件建议加括号的原因:括号不是给机器看的,是给人看的。

📐 例题 2 单分支告警:滑油油量检查

某发动机滑油油量标准为「不低于 4.5 升」。变量 oil 存着当前油量(升)。若油量低于标准,打印「警告:滑油油量偏低」;无论是否告警,最后都要打印「检查结束」。写出代码,并追踪 oil = 3.2 与 oil = 5.0 两种情形。

思路 「低于 4.5」翻译成 oil < 4.5;告警是「满足才做」,用单分支 if;「无论是否」说明这句不属于 if,不缩进。

解

oil = 3.2
if oil < 4.5:
    print("警告:滑油油量偏低")
print("检查结束")

追踪 oil = 3.2:求 3.2 < 4.5 → True,告警行执行,随后打印「检查结束」(机器实测输出这两行)。oil = 5.0:求 5.0 < 4.5 → False,缩进块整块跳过,只打印「检查结束」。

答:代码如上,两种情形的输出分别如追踪所述。⚠ 易错点:把「检查结束」也缩进进 if——那样在油量正常时它永远不打印,检查流程看起来像「卡死」;块外语句才是「无论是否都要做」的正确落点。

辨析:单分支 if 什么时候够用?

「告警后继续」和「告警后停下」是两种不同的业务:前者是提示类(油量偏低,但检查单还得走完),用单分支;后者是否决类(重量超限,直接拒绝放行),「不满足时必须做另一件事」,这就需要 else(或第 9 章的 return)。写分支前先问自己:条件不满足时,是真的什么都不用做,还是必须做另一件事?想清楚这句,if 要不要带 else 就自动清楚了。这是从「会写语法」到「会写逻辑」的第一道分水岭。

再进半步:if flag == True 为什么是坏味道

既然条件位置上放的是「真值」,那么 if flag == True: 就是绕了远路——先拿 flag 和 True 比较出一个布尔值,再让 if 对这个布尔值做真值测试。直接写 if flag:,语义完全相同、短了一半。类似地,if flag == False: 应写成 if not flag:。这不是语法错误,而是可读性坏味道:它悄悄暗示「作者还没想通布尔值本身就能当条件用」。反过来倒是提醒我们:真值测试虽好,只该用在「变量本身就是布尔含义」的场合——存的是温度、油量这类数值时,还是老老实实写 temp > 300,把比较做完整。

7.3else 与 elif:多路分支

光有 if 只能「做或不做」。要处理「二选一」,需要 else:

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

else 的含义是:「否则」——if 的条件不成立时,就执行 else 下面缩进的代码。它和 if 是一对,永远只走其中一个。

elif:三选一、多选一

当情况超过两种,用 elif(else if 的缩写,意为「否则如果」)串联:

score = 85
if score >= 90:
    print("优秀")
elif score >= 60:
    print("及格")
else:
    print("不及格")

它的执行规则是从上到下依次判断,第一个为真的分支被执行,其余全部跳过。score = 85 时:第一个条件 85≥90 为假,跳过;第二个条件 85≥60 为真,打印「及格」,后面的 else 不再执行。

三种分支结构对比
结构作用分支数
if条件真才做0 或 1 条路
if / else二选一必走 1 条
if / elif / else多选一最多走 1 条

要点 elif 可以写多个,但 else 最多一个、且必须在最后。整个 if/elif/else 链条里,最多只有一个分支会被执行——第一个满足条件的那个。

深入一步:elif 链的「拦截」本质与区间顺序

elif 链的规则只有一条:自上而下依次测试,第一个为真的分支生效,其余全部跳过。这句话里藏着一个强大的推论:排在前面的条件「拦截」了它覆盖的区间,后面的条件根本不必再管前面已经排除的情况。比如成绩五档:

if score >= 90:
    print("优秀")
elif score >= 80:
    print("良好")
elif score >= 70:
    print("中等")
elif score >= 60:
    print("及格")
else:
    print("不及格")

能走到 elif score >= 80 这一行的,必然已经不满足 >= 90——所以这里不必写 80 <= score < 90,只写下边界就够。这就是「区间条件沿同一方向排列(从高到低只写下边界,或从低到高只写上边界)」的写法。它的反面是危险的顺序错误:若把 score >= 60 挪到最前面,一个 95 分的学生会在第一关就被判成「及格」,后面所有条件再无机会执行——这不是语法错误,程序照常运行,只是逻辑错了。这种「静默的逻辑错误」比报错可怕得多:机器不吭声,结果悄悄错。

再看边界值的归属:score 恰好等于 90 走「优秀」(≥ 先到先得),恰好等于 60 走「及格」。写完任何一条 elif 链,把每个边界值各代入一次是固定动作——边界,永远是分支逻辑最容易出错的地方。

📐 例题 3 成绩五档的完整判定与边界验证

按五档标准(≥90 优秀、80–89 良好、70–79 中等、60–69 及格、<60 不及格)写 if/elif/else 链。score = 85 时输出什么?并逐一验证 95、72、60、59 的走向。

思路 从高到低排列条件,每个条件只写一个下边界;再用五个代表值把链「跑」一遍,重点照顾边界 60 与档内值。

解 代码即上面的五档链。score = 85:85 ≥ 90 为假 → 85 ≥ 80 为真 → 打印「良好」,其余分支全部跳过。

验证 把五个值逐一送入同一条链(机器实测):95→优秀,85→良好,72→中等,60→及格,59→不及格。60 恰好踩在边界上,因 >= 60 含等号而归入「及格」,与题面「60–69 及格」一致。

答:score = 85 输出「良好」;五值边界验证全部通过。⚠ 易错点:① 条件顺序颠倒导致区间被提前拦截;② ≥ 误写成 >,把恰好 90 分挡在「优秀」门外。

📐 例题 4 分段计费:行李托运费

行李托运费三段计:不超过 20 公斤免费;超过 20 公斤、不超过 40 公斤的部分每公斤 25 元;超过 40 公斤的部分每公斤 40 元。写出计费代码,并计算 18、35、45 公斤分别多少钱。

思路 三个区间正好对应 if / elif / else 三分支。关键想清楚 35 公斤怎么算:只有超出 20 的那 15 公斤计费,前 20 公斤免费。分段计费的精髓是「每一段只对它管的那部分收费」,所以第二段乘的是「超出的重量」而不是「总重量」。

解

weight = 45
if weight <= 20:
    fee = 0
elif weight <= 40:
    fee = (weight - 20) * 25
else:
    fee = (40 - 20) * 25 + (weight - 40) * 40
print("托运费:", fee, "元")

分步验算 18 公斤:18 ≤ 20 → fee = 0 元。35 公斤:不满足 ≤20、满足 ≤40 → (35−20)×25 = 15×25 = 375 元。45 公斤:前两关都不过 → 20×25 + 5×40 = 500 + 200 = 700 元。机器实测输出 0、375、700,与手算完全一致。

答:18 公斤 0 元、35 公斤 375 元、45 公斤 700 元。⚠ 易错点:① 第二段误写成 weight * 25(对全部重量收费);② 第三段漏加前段固定部分 500 元——分段计费要「逐段累加」,不是只算最后一段。

辨析:elif 链 vs 连续多个独立 if

elif 链是「互斥的多选一」:最多走一条。写成多个独立的 if,则是「各自判断、可能全中」:把五档成绩写成 5 个独立 if,85 分会同时满足 >= 80 与 >= 60 两条,打印两行。判断标准一句话:各分支互相排斥(一个值只能属于一档)用 elif 链;各条件互不干扰、可同时成立(「超重提示」和「超尺寸提示」可以同时发生)用独立 if。选错结构,逻辑从第一行就错了。

工程视角 阶梯电价、分段税率、行李费、延误赔偿……现实世界的规则大量采用「分段」结构,if/elif/else 是它最直接的代码化。写分段逻辑的黄金动作:先在纸上画一根数轴,标好每段的边界与「单价」,再动笔写代码;写完拿边界值(20、40)与段外值各验一次。纸上一分钟,省掉屏幕前一小时。

7.4底层原理:条件判断就是「跳转」

现在扒开 if 的「外衣」,看它在硬件层面到底是什么。这需要回到第 1 章的「取指-译码-执行」循环。

CPU 里有一个叫 程序计数器(Program Counter,PC)的寄存器,它始终指向「下一条要执行的指令」的地址。正常情况下,每执行完一条指令,PC 就自动加 1,指向下一条——这就是程序「从上到下」执行的硬件真相。

而 if 语句,翻译成机器码后,本质是两类指令的配合:

  1. 比较指令(CMP):比较两个数,把结果(相等、大于、小于)记到一个标志位里。
  2. 条件跳转指令(如 JE「相等就跳」、JNE「不等就跳」):根据标志位,直接改写 PC 的值,让 CPU 跳到别的地方去执行,而不是顺序往下。
条件分支 = 改写程序计数器 正常顺序时程序计数器逐条加一;遇到条件跳转时,计数器被改写成另一地址,实现分支。 程序计数器 PC 指向「下一条要执行的指令」 指令1:比较 age 和 18 指令2:若 <18 就跳转→指令5 指令3:打印"成年了" 指令4:跳转→指令6 指令5:打印"未成年" 指令6:结束 跳转 = 把 PC 从 2 改写成 5 于是 CPU 跳过 3、4,直接执行 5 这就是「分支」在硬件上的真相
图 7-2条件分支的硬件真相:一条「条件跳转指令」改写了程序计数器 PC 的值,让 CPU 跳到别处执行。

底层原理 所谓「分支」,在硬件上不过是「改写下一条指令的地址」。CPU 本身没有「选择」的意识,它只是忠实地执行「如果标志位满足,就把 PC 改到 X 地址」这条指令。你眼中「程序会做选择」,本质上是一连串机械的跳转。

🧠 一句话记忆 if 的底层 = 比较 + 条件跳转。跳转就是改程序计数器,让 CPU「不按顺序走」。

深入一步:比较不改值,只「立旗」

把 if 的底层再挖深一层。比较指令(如 CMP)有个反直觉的设计:它不产生结果数字,只是把比较的「结论」记录到几个一位的开关里——这些开关叫标志位(flags),一些资料也称条件码。最常用的三个:ZF(零标志,两数相等时亮起)、SF(符号标志,差为负时亮起)、CF(进位/借位标志,减法发生借位时亮起)。可以把标志位想象成一排指示灯:比较指令负责「点灯」,跳转指令负责「看灯办事」——JE 见 ZF 灯亮就跳,「小于就跳」的指令看 SF 与 CF 的组合。条件跳转一旦决定跳,就把「灯的结论」换成对程序计数器 PC 的一次改写。整个 if 的机器生涯,就是点一次灯、看一次灯、(可能)改一次 PC。

再看 else 的真相:机器码里根本没有 else 这个东西。所谓 else 分支,只是「if 分支末尾那条无条件跳转(JMP)所绕开的那段代码」。if/else 在高级语言里是对称的一对,在机器层面则完全不对称——一个靠条件跳转进入,一个靠无条件跳转绕过。你写 else: 时的轻松,是编译器替你安排好跳转的结果。

顺带一提:现代 CPU 为了跑得快,会在还没算完条件之前就猜一把「跳不跳」,这叫分支预测。猜对了白赚时间,猜错就得把已经开始的工作作废重来。所以「条件简单、分支规整」的代码不只是好看,也真的更快——好结构会在硬件层面收到回礼。

📐 例题 5 手工翻译:if/else 改写成「比较 + 跳转」

把下面这段代码翻译成「比较 + 条件跳转 + 无条件跳转」的伪汇编,并分别追踪 age = 16 与 age = 20 两条执行路径。

age = 16
if age >= 18:
    print("成年")
else:
    print("未成年")
print("检查结束")

思路 一条 if/else 需要五样零件:① 比较指令点灯;② 条件跳转(条件不满足时跳过 if 分支);③ if 分支本体;④ 无条件跳转(跳过 else 分支,让两路汇合);⑤ else 分支本体。汇合点之后是公共代码。

解 伪汇编如下(JL 表示「小于就跳」,JMP 表示「无条件跳」):

① CMP age, 18      # 比较:把"谁大谁小"记入标志位
② JL  ⑤            # 若 age < 18,跳到 ⑤(跳过 if 分支)
③ 打印 "成年"      # ← if 分支
④ JMP ⑥            # 无条件跳到 ⑥(绕过 else 分支)
⑤ 打印 "未成年"    # ← else 分支
⑥ 打印 "检查结束"   # ← 汇合点

路径追踪 age = 16:① 点灯(16 < 18)→ ② 灯亮,跳到 ⑤ → 打印「未成年」→ 顺势落进 ⑥ → 打印「检查结束」。age = 20:① 点灯(不小于)→ ② 不跳 → ③ 打印「成年」→ ④ 跳到 ⑥ → 打印「检查结束」。机器实测两条路径的输出分别是「未成年 / 检查结束」与「成年 / 检查结束」,与手工追踪一致。

答:五零件结构如上。else 分支由 ④ 号无条件跳转绕过——「else」在机器层面只是被绕过的代码块。⚠ 易错点:漏掉 ④ 号 JMP,age = 20 时会先打印「成年」又滑进 ⑤ 打印「未成年」,两行都出来——这正是初学汇编时最经典的错误,也反向证明了 ④ 的存在意义。

工程应用与辨析

这套「点灯 + 看灯跳转」的机制,从上世纪的真空管计算机一路沿用到今天的手机芯片,几乎没有变过。无论你将来写 Python、C 还是 Java,if 的底层都是同一件事。这给初学者一个重要的安慰:语法千差万别,内核只有一套——把「比较点灯、跳转改道」想明白,你就拿到了一把跨语言通用的钥匙。反过来,「程序跑飞」「指令乱序」这类故障,根源也常在跳转:控制流是程序的生命线,改写 PC 的权力必须被严加管束——这也是为什么高级语言要发明 if/else 这样的结构化语法,把危险的跳转权从你手里收走、交给编译器。

证据窗口:亲眼看一眼 if 的字节码

「跳转」不是比喻,是可以亲眼看到的。Python 自带一个 dis 模块,专门把代码反汇编成解释器逐条执行的指令。把 if x > 18: pass 喂给它,会看到两步关键动作:先 COMPARE_OP(做比较、点灯),紧接着 POP_JUMP_IF_FALSE——名字里赫然写着 jump:条件为假就跳到另一处继续执行(不同 Python 版本的指令名略有出入,但「比较 + 跳转」的本质不变,机器实测即此两条)。你写下的是缩进整齐的高级代码,解释器执行的仍是这套机器级老手艺——第 7 章讲的「结构」,到最底层真的就是「跳转」。

7.5布尔逻辑:把多个条件串起来

上一章学的逻辑运算符 and、or、not,在这里派上大用场——用来组合多个条件:

age = 20
has_license = True

if age >= 18 and has_license:
    print("可以开车")
else:
    print("不能开车")

这里 age >= 18 and has_license 是一个组合条件:只有当「年龄够 且 有驾照」两者都满足时,整体才是 True。

# 或:满足其一即可
if is_weekend or is_holiday:
    print("可以睡懒觉")

# 非:取反
if not is_student:
    print("不是学生,需要买全票")

组合条件的求值顺序:先算比较(age >= 18 → True),再算逻辑(True and has_license → True)。整个 if 语句只关心最终那个布尔值。

要点 and 用于「所有条件都要满足」,or 用于「任一条件满足即可」,not 用于「取反」。善用它们,你就能写出接近自然语言的复杂判断。

深入一步:短路求值——能省则省的聪明机制

and 和 or 在求值时有个重要特性:短路求值(short-circuit evaluation,第 6 章见过它的影子,这里看它在 if 里的实战)。A and B:先算 A,若 A 为假,整体必为假,B 根本不会被计算;只有 A 为真才轮到 B。A or B 反过来:A 为真则整体必真,B 被短路。像电路一样记:and 是串联——断一处全断;or 是并联——通一处全通。

短路不只是省时间,它是一种保护机制。经典场景是「先确认、再使用」:判断「记录存在 且 数值达标」时,前半句为假(没有记录)就不该再去碰「数值」——短路让后半句自动免于执行,天然避免「对着不存在的东西做检查」。工程代码里大量防御式判断,都靠这个机制默默兜底。

再补一条进阶事实:and/or 返回的不一定是 True/False,而是「决定结果的那个操作数」——比如 0 or "备用" 的结果是 "备用"。初学阶段不必深究,记住口诀「and 找假、or 找真、找到即返回」即可,它同时也是理解短路的钥匙。

📐 例题 6 复合条件:闰年判断

历法规则:能被 4 整除且不能被 100 整除,或者能被 400 整除的年份是闰年。请用一个 if/else 写出判断,并验证 2024、1900、2000 三个年份。

思路 先拆命题:A =「被 4 整除且不被 100 整除」,B =「被 400 整除」,整句是「A 或 B」,即 (A) or (B)。「整除」用 % 表达(余数为 0)。注意 and 的优先级高于 or——a and b or c 会按 (a and b) or c 计算,本例恰好正确,但为了可读性与稳妥,建议显式加括号。

解

year = 2024
if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:
    print(year, "是闰年")
else:
    print(year, "不是闰年")

分步验算 2024:2024 % 4 = 0,且 2024 % 100 = 24 ≠ 0 → A 真 → 闰年。1900:1900 % 4 = 0,但 1900 % 100 = 0 → A 假;1900 % 400 = 300 ≠ 0 → B 假 → 平年。2000:A 假(被 100 整除),但 2000 % 400 = 0 → B 真 → 闰年。机器实测与标准库 calendar.isleap 的判定完全一致(True / False / True)。

答:2024 闰年、1900 平年、2000 闰年。⚠ 易错点:漏掉「或被 400 整除」这半句,会把 2000 年错判成平年——复合条件最容易丢的就是 or 的另一半;另注意「不能被 100 整除」要写 != 0,写反成 == 0 就变成另一个命题。

三个高频布尔陷阱

布尔表达式有三个新手陷阱,值得单独点名:

  1. 把 == 写成 =。if x == 5: 是问「x 等于 5 吗」,if x = 5: 则试图在条件里赋值——Python 直接报 SyntaxError(机器实测如此,报错信息还贴心提示「你是不是想写 ==」)。不少语言里这个笔误会悄悄变成赋值、让条件恒真,酿成极难发现的 bug;Python 在语法层面帮你挡住了这个坑,这是它对新手最实在的善意之一。
  2. 链式比较是合法的优雅写法。if 18 <= age < 60: 完全等价于 18 <= age and age < 60,读起来就是数学语言(机器实测:age 为 38 时为 True)。但别顺手写出 18 < age > 60——它语法合法,语义却是「age > 18 且 age > 60」,与「18 到 60 之间」毫无关系。合法≠合理,链式比较只留给真正的区间。
  3. 浮点数别用 == 判相等。0.1 + 0.2 == 0.3 的结果是 False——第 5 章讲过,二进制浮点存不下精确的 0.1 与 0.2,误差在运算中暴露。正确姿势是「容差」:abs(0.1 + 0.2 - 0.3) < 1e-9,即「差小到可忽略就算相等」(机器实测 True)。凡是浮点比较,一律想到容差。

再补一个优先级细节:not 的优先级低于比较运算,所以 not 3 == 5 等价于 not (3 == 5)——先比较、再取反(机器实测为 True)。拿不准时,括号永远是最可靠的朋友。

工程视角 航空场景里复合条件无处不在:放行需要「舱门已关闭 and 油量达标 and 无未处理告警」,告警则是「超温 or 超压 or 传感器失效」任一成立即触发。真实系统的逻辑判断表,拆开看全是本节这三个运算符的组合——把 and/or/not 用准,就是给系统的安全逻辑上保险。

辨析:not 的位置决定含义

not (a and b) 与 (not a) and (not b) 是两回事:前者是「两者不同时成立」,后者是「两个都不成立」。逻辑学里的德摩根定律能把它们互相改写:not (A and B) 等价于 (not A) or (not B)。写复杂否定条件时,先用这条定律把 not 推到每个条件跟前再取反,往往比硬憋一个巨型括号清爽得多——「先化简、再编码」,是布尔逻辑的通用工作法。

短路的保护用法:先守门,再计算

短路还有一种极实用的保镖写法:把「便宜的检查」放左边,「危险的计算」放右边。比如计算速度比必须除以 speed,就必须先确认它不是 0,否则会抛出除零错误:

for speed in [0, 2]:
    if speed != 0 and 100 / speed > 5:
        print("速度比偏高")
    print("speed =", speed, "检查完成")

speed 为 0 时,左半 speed != 0 已经是 False,and 当场短路,100 / speed 根本不会被求值——除零错误被挡在门外(机器实测:speed=0 只打印「检查完成」,speed=2 两行都打印)。这招在工程代码里随处可见:先判空再取值、先判零再除法、先判存在再访问。「守门员站左边,攻手站右边」,是复合条件的排兵布阵口诀。

7.6缩进:Python 的「代码块」

你可能已经注意到,Python 的 if 语句里,属于 if 的代码要缩进(前面空几个空格)。这不是排版习惯,而是 Python 的语法规则。

在 Python 里,缩进决定「哪些代码属于同一个代码块」。同一层缩进的代码,属于同一个块,一起执行、一起被跳过。

if age >= 18:
    print("第一句")      # 缩进了,属于 if
    print("第二句")      # 同样缩进,也属于 if
print("第三句")          # 没缩进,不属于 if,永远执行

当 age = 15 时,前两句被一起跳过,只打印「第三句」;当 age = 20 时,三句都打印。

缩进划分代码块 缩进相同的语句属于同一个代码块,不缩进的语句在块外。 if age >= 18: print("第一句") print("第二句") print("第三句") │ 缩进块 = if 的代码块 ↑ 没缩进 = 块外,永远执行
图 7-3缩进相同的是同一个代码块;没缩进的就是块外。Python 靠缩进识别「谁属于谁」。

⚠️ 最容易犯的错 Python 对缩进极其严格:缩进不一致会直接报错(如 IndentationError)。建议统一用 4 个空格作为一层缩进,不要混用空格和 Tab 键,否则会出现「看着一样、实则不同」的隐形错误。

📖 为什么 Python 用缩进,而不是大括号? 很多语言(如 C、Java)用大括号 { } 表示代码块。Python 的设计者 Guido 认为,反正程序员都会习惯性缩进,不如干脆「用缩进本身」当作语法,这样代码强制整齐、人人一致。代价是缩进必须严格正确——这恰恰逼你养成好习惯。

深入一步:缩进是如何被「读」进去的

Python 读你的代码时,会先把文本切成一个个记号(token)。对缩进的处理发生在很早的词法阶段:行首突然多出的空白会被切出一个 INDENT 记号,突然变少会切出 DEDENT 记号。语法分析器就靠这两个记号知道「这里新开了一层块」「这里退回上一层」——缩进不是注释、不是装饰,而是语法结构本身。这也解释了为什么「看着对」不行:Tab 和空格在屏幕上几乎一样,对词法器却是两个不同的字符,混用等于埋雷。

嵌套的分支带来嵌套的缩进:if 里再套 if,每深一层就右移一格。层级越深,读者脑子里要同时挂起的「上下文」越多。工程经验法则是嵌套尽量不超过 3 层;更深时优先考虑改写——比如把多层嵌套改成一串 elif(就像 7.3 节的五档链,天然只有一层),或把内层逻辑打包成函数(第 9 章)。「缩进深度」甚至被当作代码复杂度的直观量尺:一屏装不下你的缩进,多半说明逻辑该拆了。

📐 例题 7 嵌套 if 读代码:预测输出

不运行代码,先预测 temp = 38 时的输出,并解释原因:

temp = 38
level = "正常"
if temp > 37:
    level = "偏高"
    if temp >= 39:
        level = "高热"
print("结论:", level)

思路 盯住缩进定归属:level = "偏高" 与整个内层 if 都缩进一层,属于外层 if;level = "高热" 再缩进一层,属于内层 if;最后的 print 没缩进,永远执行。

解 temp = 38:38 > 37 为真 → 进入外层块,level 先被改成「偏高」;再看内层 38 ≥ 39 为假 → 「高热」不执行;退出全部 if,打印「结论: 偏高」。机器实测三组验证:38 → 偏高;39.5 → 高热;36.5 → 正常。

答:输出「结论: 偏高」。⚠ 易错点:以为内层没执行会连「偏高」也一并跳过——「偏高」属于外层 if,只要外层条件真就会执行;内层失败只影响「高热」一句。读嵌套代码的口诀:先看这句缩进到第几层、属于谁,再看条件真假。

嵌套 if 与缩进层级 嵌套 if 的代码随层级右移,缩进深度代表所属层级。 temp = 38 if temp > 37: level = "偏高" if temp >= 39: level = "高热" print("结论:", level) 第 1 层:外层 if 的块(含 3 句) 第 2 层:内层 if 的块(仅 1 句) 没缩进 → 不属于任何 if,永远执行
图 7-4嵌套 if:缩进每深一层,就多一层「属于」关系——内层 if 整体住在外层 if 的代码块里。

辨析:缩进是语法,风格是风格

分清两件事:缩进的层级由语法强制——错了就报错,或逻辑悄悄变;缩进的宽度(一层 4 格还是 2 格)是风格约定——Python 官方风格指南 PEP 8 建议 4 个空格,全世界的 Python 代码基本都遵守它,入乡随俗即可。真正不能碰的红线只有两条:同一块内缩进必须一致;Tab 与空格不能混。现代编辑器都能把按下的 Tab 自动换算成 4 个空格,确认这个设置打开,就能从源头免疫一大类「看着一样、实则不同」的隐形报错。

混用 Tab 与空格:专属报错 TabError

「不能混」不是口头警告——同一文件里同一层缩进有时用 Tab 有时用空格,Python 会直接拒绝编译,并给出一条专属报错:TabError: inconsistent use of tabs and spaces in indentation(机器实测原文如此),比普通的 IndentationError 更明确地指向「混用」。混用的常见来源不是手敲,而是从网页或聊天窗口复制代码时悄悄带进来的真实 Tab 字符。遇到 TabError 不必慌:在编辑器里打开「显示空白字符」,把整块代码重新统一缩进一遍(全空格最省心),报错即消失。看到报错能一秒定位到「缩进混用」,你就比大多数初学者快了一步。

本章小结

  1. 条件分支让程序能根据数据做不同反应,是程序「智能感」的第一个来源。
  2. if 条件为真才执行;else 二选一;elif 多选一(最多走一条)。
  3. 分支的底层原理是「比较 + 条件跳转」——跳转就是改写程序计数器 PC。
  4. and/or/not 可组合多个条件,构成复杂的布尔判断。
  5. Python 用缩进划分代码块,缩进错误会报错;建议统一用 4 个空格。
  6. elif 链自上而下「先到先得」:区间条件沿同一方向排列、每个条件只写一个边界;顺序写反不报错、只出错——写完用边界值逐一检验。
  7. 布尔三大陷阱:= 与 == 天壤之别;链式比较 18 <= x < 60 合法且优雅;浮点判等要用容差 abs(a - b) < 1e-9,而不是 ==。

读完本章,你应该能……

思考题

1. 用 if/else 写一个程序:输入一个数,判断它是奇数还是偶数(提示:用 %)。

提示:if x % 2 == 0: print("偶数") else: print("奇数")。

2. if score >= 90 和 elif score >= 60 里,一个 95 分的学生会走哪条分支?为什么?

提示:走第一条(优秀),因为 elif 链条是「从上到下,第一个为真的分支生效,其余跳过」。

3. 用自己的话解释:CPU 是怎么实现「程序走不同的路」的?

提示:通过比较指令得到标志位,再由条件跳转指令改写程序计数器 PC,让 CPU 跳到不同地址执行。

4. 为什么 if x > 0 下面必须缩进?缩进错了会怎样?

提示:缩进是 Python 划分代码块的语法;缩进错误会报 IndentationError。

5. 行李计费规则若改为「不超过 20 公斤免费,超出部分一律每公斤 25 元」,例题 4 的代码要怎样简化?45 公斤要花多少钱?

提示:不再需要三段,只剩一个条件:if weight > 20: fee = (weight - 20) * 25,否则 fee = 0。45 公斤:(45−20)×25 = 625 元(代码验算 625)。规则简化,代码跟着简化——代码结构应精确映照规则结构。

6. if 18 <= age < 60: 与 if 18 < age > 60: 各是什么意思?后者能用来判断「年龄在 18 到 60 之间」吗?

提示:前者是区间判断(等价于 18 <= age and age < 60)。后者等价于 age > 18 and age > 60,语义是「大于 60」——语法合法但语义混乱,不能表达区间。

7. 为什么机器码里找不到「else」?else 分支在硬件层面靠什么实现?

提示:else 只是「if 分支末尾那条无条件跳转(JMP)所绕开的代码块」。硬件层面只有比较(置标志位)、条件跳转与无条件跳转三样东西。