第 08 章 · 条件判断:是否达到退役线
这一章要解决什么:给每支电芯判一个健康档位 —— 健康、预警,还是该退役了。 这一章要打掉两个坑:分支顺序写反(不报错但结论全错)、哪些值算
False。
本章你需要完成的代码:demos/ch08-condition.py
8.1 二档:if / else
caps = [2.50, 2.30, 2.12, 2.00, 1.95]
cap_rated = 2.5
for c in caps:
soh = c / cap_rated
if soh < 0.80:
status = "待退役"
else:
status = "健康"
print(f" SOH {soh:.3f} → {status}")输出:
SOH 1.000 → 健康
SOH 0.920 → 健康
SOH 0.848 → 健康
SOH 0.800 → 健康
SOH 0.780 → 待退役| 部分 | 作用 |
|---|---|
if 条件: | 条件为真就执行下面缩进的块 |
else: | 条件为假时执行(可以没有) |
冒号 : | 每个分支的结尾都要有,且必须是半角 |
| 缩进 4 格 | 谁属于这个分支,由缩进决定 |
注意 SOH 0.800 → 健康:条件是 soh < 0.80(严格小于),0.80 本身不小于 0.80。 边界归哪一档是业务决策,不是语法 —— 定阈值时一定要确认"等于的时候算什么"。
8.2 三档:分支顺序必须"从严格到宽松"
if soh < 0.80: # 最严格放最前
status = "待退役"
elif soh < 0.85: # 次严格
status = "预警"
else: # 剩下的都归这里
status = "健康"输出:
SOH 1.000 → 健康
SOH 0.920 → 健康
SOH 0.848 → 预警
SOH 0.800 → 预警
SOH 0.780 → 待退役elif 是 "else if" 的缩写:前面的条件都不成立时,才轮到它。
点开:顺序写反会怎样?(本章唯一真正的考点)
# ❌ 把宽松的条件写在前面
if soh < 0.85:
status = "预警"
elif soh < 0.80:
status = "待退役" # 永远不会被执行到!
else:
status = "健康"0.78 也满足 soh < 0.85,第一分支就把它接走了,永远轮不到"待退役"。 而且这段代码不报错、不警告,跑起来一切正常 —— 只是该退役的电芯都被标成了"预警"。
demo 里的对照:
SOH 0.78 → 写反得到「预警」,正确是「待退役」
SOH 0.84 → 写反得到「预警」,正确是「预警」
SOH 0.95 → 写反得到「健康」,正确是「健康」注意后两行:0.84 和 0.95 两种写法结果一致。顺序写反只在一部分数据上出错 —— 这正是它难被发现的原因。
判断规则:elif 里的条件,隐含了"前面所有条件都不成立"。心里补一句话:
if soh < 0.80: # 走到这里:任意 SOH
elif soh < 0.85: # 走到这里:说明 soh >= 0.80
else: # 走到这里:说明 soh >= 0.85写 elif 的自律:条件要么越来越宽松、要么越来越严格,不能来回横跳。 不确定就把每档区间写成注释。
8.3 比较运算符与链式比较
soh = 0.84
print(soh < 0.85) # True
print(soh == 0.84) # True ← 判断相等是两个等号
print(0.80 <= soh <= 0.85) # True ← Python 支持链式比较
print(soh != 0.9) # True| 运算符 | 含义 | 备注 |
|---|---|---|
== | 等于 | 一个 = 是赋值,这是最常见的笔误 |
!= | 不等于 | |
< <= > >= | 大小比较 | |
in / not in | 在不在里面 | "C" in "C001" |
链式比较 0.80 <= soh <= 0.85 是 Python 的特色,等价于 0.80 <= soh and soh <= 0.85,但只写一次 soh,更短也更清楚。
别用 == 比较浮点数(第 04 章):
print(0.1 + 0.2 == 0.3) # False ← 浮点误差
print(abs((0.1 + 0.2) - 0.3) < 1e-9) # True ← 正确比法== 比的是值,is 比的是"同一个对象"(第 06 章)。 永远别用 is 比数字 —— Python 会直接给出 SyntaxWarning。
8.4 and / or / not 与短路求值
temp, r_int = 32.0, 85.0
print(temp > 30 and r_int > 80) # True 两边都真才是真
print(temp > 40 or r_int > 80) # True 一边真就是真
print(not (soh < 0.85)) # False 取反| 运算符 | 什么时候为真 | 优先级 |
|---|---|---|
and | 两边都真 | 高于 or |
or | 至少一边真 | 低于 and |
not | 条件为假时 | 最高(比较之后、and 之前) |
优先级记法:not > and > or,拿不准就加括号。 a or b and c 是 a or (b and c),不是 (a or b) and c。
短路求值:and 左边为假时,右边根本不执行;or 左边为真时同理。
caps = []
print(caps and caps[0] > 0) # [] ← 左边为空列表(假),右边不执行,不会 IndexError
print(caps[0]) # IndexError —— 少了这个保护就崩了"先判空、再取值"是短路求值最实用的场景。
⚠ 副作用:如果右边有副作用(函数调用、计数),它可能压根不会发生:
def log_and_check(x): # 一个有"副作用"的函数:会打印一行
print("我被调用了")
return x < 0.80
soh = 0.92
print(soh < 0.80 and log_and_check(soh)) # False,右边没执行 → 不打印"我被调用了"
print(soh > 0.80 or log_and_check(soh)) # True,右边同样没执行调试"为什么这个函数没被调用"时,先看短路。
and / or 返回的是"操作数",不是 True / False:
print("A" and "B") # B ← 左边真,返回右边
print("" or "兜底") # 兜底 ← 左边假,返回右边这是个非常实用的写法:name = user_input or "匿名" —— 空字符串时用默认值。 (第 09 章的 dict.get(key, 默认值) 是同一类思路。)
8.5 什么算 False:真值测试
for v in [0, 0.0, "", [], {}, None, False]:
print(f" bool({v!r:<8}) = {bool(v)!r}")输出:
bool(0 ) = False
bool(0.0 ) = False
bool('' ) = False
bool([] ) = False
bool({} ) = False
bool(None ) = False
bool(False ) = False规律:空的东西、零、None 都是 False。
是 False | 是 True |
|---|---|
0、0.0 | 任何非零数字(-1 也是 True) |
""(空字符串) | "0"、"False"(非空字符串都是 True) |
[]、{}、() | 只要有元素就是 True([0] 也是 True) |
None | 任何对象 |
所以 if caps: 等价于 if len(caps) > 0:,但更 Pythonic。
⚠ 最常见的坑是 "0" 和 "False":
s = "False"
if s: # True!非空字符串永远为真
print("进到这里了")从文件/命令行/网页读进来的布尔值一定是字符串,要显式转换: s.lower() in ("1", "true", "yes")。
另一个坑:数值 0 会被当成"没有值"。如果 0 对你是合法数据(比如温升 0℃), 就必须写 if temp is not None:,不能用 if temp:。
8.6 条件表达式(三元):简单分支写一行
status = "健康" if soh >= 0.80 else "待退役"读法:A if 条件 else B —— 条件真取 A,否则取 B。等价的 if/else 要写 4 行。
| 什么时候用 | 什么时候别用 |
|---|---|
| 一个简单的二选一赋值 | 需要 elif、或嵌套超过一层 |
| 放进 f-string / 列表推导式里 | 逻辑有副作用(调用函数、改数据) |
嵌套三元是"可读性杀手":
status = "待退役" if soh < 0.80 else ("预警" if soh < 0.85 else "健康") # ⚠ 勉强能读demo 里特意说了:"嵌套太多层就别用了,回到 if/elif/else —— 可读性优先。"
8.7 match:多分支时的清爽写法(Python 3.10+)
for code in (0, 1, 2, 9):
match code:
case 0:
note = "正常"
case 1:
note = "预警"
case 2:
note = "待退役"
case _: # _ 是兜底,相当于 else
note = "未知状态码"
print(f" 状态码 {code} → {note}")输出:
状态码 0 → 正常
状态码 1 → 预警
状态码 2 → 待退役
状态码 9 → 未知状态码match 适合"一个值对多个固定选项"(状态码、命令、枚举):
if/elif/else | match/case | |
|---|---|---|
| 判断范围 | 任何条件(<、in、and) | 主要对值 / 结构做匹配 |
| 兜底 | else | case _: |
| 版本 | 所有版本 | Python 3.10+ |
本书的阈值判断仍然用 if/elif/else,因为那些条件是范围判断(<、>=),不是"值等于什么"。 注意 case _ 不能少,否则没有匹配的值会什么都不做(不报错),程序悄悄跳过 —— 又一个静默 bug。
8.8 缩进就是语法
soh = 0.78
if soh < 0.80:
print("待退役") # ❌ IndentationError: expected an indented blockPython 用缩进表示"谁属于谁",这是它和其他语言最大的外观差异。
| 错误 | 报错 | 后果 |
|---|---|---|
| 该缩进没缩进 | IndentationError | 程序直接停 |
| Tab 和空格混用 | TabError | 程序直接停 |
| 多缩进/少缩进一层 | 不报错 | 逻辑跑到了 if 外面,结果错 |
第三种最危险:
for c in caps:
soh = c / cap_rated
if soh < 0.80: # ← 少缩进了!它跑到循环外面了
print(f"SOH {soh:.3f} → 待退役")这段代码只在循环结束后判断了一次(用的是最后一支电芯的 SOH),而不是每支都判断。 它不报错,只是结果错。
纪律:统一用 4 个空格(PEP 8),别用 Tab;编辑器里打开"显示空白字符"; 写 if / for 后马上一按回车,让编辑器自动缩进。
8.9 完整示例与输出
cd code
python demos/ch08-condition.py真实输出(节选):
=== ① 二档:是否到退役线 ===
SOH 1.000 → 健康
SOH 0.920 → 健康
SOH 0.848 → 健康
SOH 0.800 → 健康
SOH 0.780 → 待退役
=== ② 三档:顺序就是考点 ===
SOH 1.000 → 健康
SOH 0.920 → 健康
SOH 0.848 → 预警
SOH 0.800 → 预警
SOH 0.780 → 待退役
如果顺序写反(把 <0.85 放最前)会怎样?
SOH 0.78 → 写反得到「预警」,正确是「待退役」
SOH 0.84 → 写反得到「预警」,正确是「预警」
SOH 0.95 → 写反得到「健康」,正确是「健康」
=== ③ 比较运算 ===
soh < 0.85 : True
soh == 0.84 : True ← 判断相等用两个等号
0.80 <= soh <= 0.85 : True ← Python 支持链式比较
=== ④ 逻辑运算 ===
温升 >30 且 内阻 >80 ? True
短路:and 左边为假就不再算右边(右边有副作用时要注意)
=== ⑤ 什么算 False ===
bool(0 ) = False
bool('' ) = False
bool([] ) = False
bool({} ) = False
bool(None ) = False
所以 if caps: 就等于 if len(caps) > 0:,更 Pythonic
=== ⑥ 条件表达式 ===
SOH 0.780 → 待退役
嵌套太多层就别用了,回到 if/elif/else —— 可读性优先。
=== ⑦ match(Python 3.10+) ===
状态码 0 → 正常
状态码 1 → 预警
状态码 2 → 待退役
状态码 9 → 未知状态码
=== ⑧ 缩进错误 ===
Tab 和空格混用 → TabError / IndentationError
该缩进没缩进 → 逻辑跑到 if 外面去了(不报错,但结果错,最危险)对照看 ① 和 ②:同一批数据,①里 0.848 是"健康",②里是"预警" —— 因为②多了"预警档"。 阈值一改,历史结论就可能跟着变,所以真实的电池报告里必须写清"用的是哪一版判定规则" (第 34 章会讲策略)。
8.10 常见坑
坑 ①:分支顺序写反 —— 本章第一号坑。if soh < 0.85 写在 if soh < 0.80 前面,"待退役"永远不可达,且不报错。
坑 ②:= 写成了 ==,或反过来。
if soh = 0.80: # ❌ SyntaxError: invalid syntax
soh == 0.80 # ❌ 这只是一句比较,值被丢掉了(不报错!)第二种更隐蔽:它不报错,只是什么都没发生。
坑 ③:用 == 比较浮点数。 0.1 + 0.2 == 0.3 是 False,用 abs(a - b) < 1e-9(第 04 章)。
坑 ④:and / or 优先级搞混。
if a > 1 or b > 2 and c > 3: # ❌ 实际是 a > 1 or (b > 2 and c > 3)
if (a > 1 or b > 2) and c > 3: # ✅ 加括号,意图明确坑 ⑤:把字符串 "False" 当成 False。 非空字符串永远为真。要显式判断 s == "False"。
坑 ⑥:用 if x: 判断一个可能为 0 的数值。 0 会被当成"假", 合法数据包含 0 时要写 if x is not None:。
坑 ⑦:以为 and / or 一定返回 True / False。 "A" and "B" 返回 "B"。
坑 ⑧:缩进层级写错,逻辑跑到 if 外面。 不报错、结果错。
坑 ⑨:全角冒号/括号。 if soh < 0.80: 报 SyntaxError: invalid character ':' —— 和第 05 章同一个坑:写代码时切英文输入法。
本章练习
练习 1:把二档判断改成三档
已知 6 支电芯的容量(额定 2.5 Ah):
cap_rated = 2.5
caps = [2.50, 2.30, 2.21, 2.17, 2.12, 1.95]把二档判断改成三档:SOH < 0.80 → 待退役;SOH < 0.85 → 预警;其余 → 健康。 打印每支的状态(SOH 左对齐宽 6、保留 3 位),并统计每档各几支。
期望输出:
SOH 1.000 → 健康
SOH 0.920 → 健康
SOH 0.884 → 健康
SOH 0.868 → 健康
SOH 0.848 → 预警
SOH 0.780 → 待退役
(校验){'健康': 4, '预警': 1, '待退役': 1}参考答案
cap_rated = 2.5
caps = [2.50, 2.30, 2.21, 2.17, 2.12, 1.95]
counts = {"健康": 0, "预警": 0, "待退役": 0}
for c in caps:
soh = c / cap_rated
# ★ 顺序必须是"从最严格到最宽松"
if soh < 0.80: # 走到这里:任意 SOH
status = "待退役"
elif soh < 0.85: # 走到这里:说明 soh >= 0.80
status = "预警"
else: # 走到这里:说明 soh >= 0.85
status = "健康"
counts[status] += 1
print(f"SOH {soh:<6.3f} → {status}")
print(f"\n(校验){counts}")要点解释:
- 分支顺序是本题唯一真正的考点。先判最严格的
< 0.80,再判< 0.85。 写反了0.780会被标成"预警",而且代码不报错。 {soh:<6.3f}里<6只影响显示宽度,不改变数值,也不影响比较。counts[status] += 1用字典计数(第 09 章):注意它在循环外面初始化 —— 放里面每次都会被清零。- 边界要确认:
0.85本身走else(健康),0.80本身走"预警"。
完整练习在 exercises/ex04-08-condition。
练习 2:找出下面这段代码的 bug
def classify(soh):
if soh < 0.85:
return "预警"
elif soh < 0.80:
return "待退役"
else:
return "健康"
for soh in (0.78, 0.84, 0.95):
print(f"SOH {soh} → {classify(soh)}")要求:不要运行,先说出每支电芯的输出,再指出代码的问题,最后给出修正版。
参考答案
实际输出:
SOH 0.78 → 预警
SOH 0.84 → 预警
SOH 0.95 → 健康问题:0.78 应该是「待退役」,却得到「预警」。 因为 0.78 < 0.85 成立,第一个分支就把它接走了,elif soh < 0.80 永远不可达。
这类 bug 的三个特点:
| 特点 | 说明 |
|---|---|
| 不报错 | 语法完全合法,程序正常跑完 |
| 不总是错 | 0.84、0.95 的结果都是对的,只有"更严重"的那档出问题 |
| 方向单一 | 错误总是把"严重"的样本标成"较轻"的档 —— 预警/筛查场景里后果很大 |
修正:把条件按"从严格到宽松"重排:
def classify(soh):
if soh < 0.80: # 最严格的最先判
return "待退役"
elif soh < 0.85:
return "预警"
else:
return "健康"修正后输出:0.78 → 待退役、0.84 → 预警、0.95 → 健康。
怎么预防:写完就把每一档的区间写成注释,或者干脆用"区间判断"而不是"阈值判断":
if 0.80 <= soh < 0.85: # 链式比较,区间写得明明白白
return "预警"练习 3:预测输出(不要运行,先想)
print(0.80 <= 0.84 <= 0.85)
print(bool(0), bool("0"), bool([]), bool([0]))
print("A" and "B")
print("" or "兜底")
print("健康" if 0.78 >= 0.80 else "待退役")
caps = []
print(caps and caps[0] > 0)
print(1 == 1.0)
print(1 is 1.0)要求:先写下每一行的输出,再运行验证。不一致的地方,说出原因。
参考答案
预测输出:
True
False True False True
B
兜底
待退役
[]
True
False| 表达式 | 结果 | 为什么 |
|---|---|---|
0.80 <= 0.84 <= 0.85 | True | 链式比较 = 0.80 <= 0.84 and 0.84 <= 0.85 |
bool(0), bool("0") | False True | 数字 0 是假;字符串 "0" 非空,是真 |
bool([]), bool([0]) | False True | 空列表是假;含一个 0 的列表非空,是真 |
"A" and "B" | 'B' | and 返回操作数:左边真 → 返回右边 |
"" or "兜底" | '兜底' | or 返回操作数:左边假 → 返回右边 |
"健康" if 0.78 >= 0.80 else "待退役" | '待退役' | 条件为假,取 else 那一边 |
caps and caps[0] > 0 | [] | 短路:左边空列表为假 → 右边不执行,返回 [](不会 IndexError) |
1 == 1.0 | True | == 比值:整数 1 和浮点 1.0 值相等 |
1 is 1.0 | False | is 比对象身份;还会触发 SyntaxWarning,别这么写 |
三处最容易错的:
bool("0")是True、bool([0])是True—— 判断依据是"空不空",不是"里面是不是 0"。 所以"从配置里读到的"0""在if里会被当成真。and/or返回操作数本身,不是True/False—— 这也是x = user_input or "默认值"能工作的原因。caps and caps[0] > 0不报错:短路求值救了它。写成caps[0] > 0 and caps就会先caps[0]→IndexError。"先判空、再取值"的顺序不能颠倒。
面向 AI 开发:条件判断就是"给数据贴标签"
AI 项目的一半工作量在"判断"上:判断样本合不合法、判断该进哪个分支、 判断异常还是正常、判断该不该停止训练。
① 数据分档 / 贴标签(labeling)—— 本章的主线场景。
df["health"] = "健康"
df.loc[df["soh"] < 0.85, "health"] = "预警"
df.loc[df["soh"] < 0.80, "health"] = "待退役"这是第 27 章"派生列"的写法 —— 三行的顺序决定了最终结果。 Pandas 里更安全的做法是 pd.cut:
df["health"] = pd.cut(df["soh"], bins=[0, 0.80, 0.85, 1.0],
labels=["待退役", "预警", "健康"], right=False)pd.cut 的好处是"每一档的区间写在一处",从根上避免顺序写反。
② 布尔掩码(boolean mask)—— 条件判断的向量化版本。
mask = df["soh"] < 0.85 # 一整列的比较,得到一列 True/False
bad = df[mask] # 用掩码筛行(第 26 章)
mask = (df["temp_rise_c"] > 30) & (df["resistance_mohm"] > 80) # 用 & 不用 and!⚠ Pandas / NumPy 里必须用 & | ~,不能用 and or not —— and 会对整列求"真值",直接报 ValueError: The truth value of a Series is ambiguous。 括号也不能省(& 优先级高于 >)。
③ 短路求值 = 防崩护栏。
if caps and caps[0] > 0: # 先判空,再取值
if df is not None and len(df) > 0: # 先判 None,再判长度数据管道里到处是"可能为空 / 可能为 None"的中间结果, 短路求值让"先检查、再用"写在一行里,不必嵌套一层 if。
④ 训练里的条件判断:早停、保存最优模型、跳过坏批次。
if val_loss < best_loss - 1e-4: # 有改善才保存
best_loss = val_loss
save_model()
elif patience > 5: # 连续多次没改善
break # 早停(第 07 章的 break)这里必须用 - 1e-4 这样的 epsilon(第 04 章): 浮点损失几乎不会"精确变小",直接写 val_loss < best_loss 会让早停逻辑抖动。
⑤ 条件判断的"错误方向"在 AI 里代价不对称。
医学筛查、故障预警、电池退役判断都属于"漏报比误报贵得多"的场景:
| 错误类型 | 例子 | 代价 |
|---|---|---|
| 漏报(假阴性) | 该退役的电芯被标成"预警" | 安全事故 |
| 误报(假阳性) | 健康电芯被标成"待退役" | 多检查一次 |
分支顺序写反正好制造漏报(把严重的归到较轻的档)—— 这就是本章那个"不报错的 bug"为什么值得单独讲一节。
一句话总结:条件判断在 AI 项目里就是给数据贴标签。 语法只有 if/elif/else 这么点,难点全在业务规则怎么定、边界归哪档、顺序对不对。 把每一档的区间写下来、把顺序检查一遍,这两件事比背语法重要得多。