Skip to content

第 08 章 · 条件判断:是否达到退役线 ​

这一章要解决什么:给每支电芯判一个健康档位 —— 健康、预警,还是该退役了。 这一章要打掉两个坑:分支顺序写反(不报错但结论全错)、哪些值算 False。

本章你需要完成的代码:demos/ch08-condition.py

8.1 二档:if / else ​

python
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}")

输出:

text
  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 三档:分支顺序必须"从严格到宽松" ​

python
if soh < 0.80:            # 最严格放最前
    status = "待退役"
elif soh < 0.85:          # 次严格
    status = "预警"
else:                     # 剩下的都归这里
    status = "健康"

输出:

text
  SOH 1.000  → 健康
  SOH 0.920  → 健康
  SOH 0.848  → 预警
  SOH 0.800  → 预警
  SOH 0.780  → 待退役

elif 是 "else if" 的缩写:前面的条件都不成立时,才轮到它。

点开:顺序写反会怎样?(本章唯一真正的考点)
python
# ❌ 把宽松的条件写在前面
if soh < 0.85:
    status = "预警"
elif soh < 0.80:
    status = "待退役"        # 永远不会被执行到!
else:
    status = "健康"

0.78 也满足 soh < 0.85,第一分支就把它接走了,永远轮不到"待退役"。 而且这段代码不报错、不警告,跑起来一切正常 —— 只是该退役的电芯都被标成了"预警"。

demo 里的对照:

text
  SOH 0.78 → 写反得到「预警」,正确是「待退役」
  SOH 0.84 → 写反得到「预警」,正确是「预警」
  SOH 0.95 → 写反得到「健康」,正确是「健康」

注意后两行:0.84 和 0.95 两种写法结果一致。顺序写反只在一部分数据上出错 —— 这正是它难被发现的原因。

判断规则:elif 里的条件,隐含了"前面所有条件都不成立"。心里补一句话:

text
if soh < 0.80:                    # 走到这里:任意 SOH
elif soh < 0.85:                  # 走到这里:说明 soh >= 0.80
else:                             # 走到这里:说明 soh >= 0.85

写 elif 的自律:条件要么越来越宽松、要么越来越严格,不能来回横跳。 不确定就把每档区间写成注释。

8.3 比较运算符与链式比较 ​

python
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 章):

python
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 与短路求值 ​

python
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 左边为真时同理。

python
caps = []
print(caps and caps[0] > 0)      # []  ← 左边为空列表(假),右边不执行,不会 IndexError
print(caps[0])                   # IndexError —— 少了这个保护就崩了

"先判空、再取值"是短路求值最实用的场景。

⚠ 副作用:如果右边有副作用(函数调用、计数),它可能压根不会发生:

python
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:

python
print("A" and "B")     # B      ← 左边真,返回右边
print("" or "兜底")     # 兜底    ← 左边假,返回右边

这是个非常实用的写法:name = user_input or "匿名" —— 空字符串时用默认值。 (第 09 章的 dict.get(key, 默认值) 是同一类思路。)

8.5 什么算 False:真值测试 ​

python
for v in [0, 0.0, "", [], {}, None, False]:
    print(f"  bool({v!r:<8}) = {bool(v)!r}")

输出:

text
  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":

python
s = "False"
if s:                  # True!非空字符串永远为真
    print("进到这里了")

从文件/命令行/网页读进来的布尔值一定是字符串,要显式转换: s.lower() in ("1", "true", "yes")。

另一个坑:数值 0 会被当成"没有值"。如果 0 对你是合法数据(比如温升 0℃), 就必须写 if temp is not None:,不能用 if temp:。

8.6 条件表达式(三元):简单分支写一行 ​

python
status = "健康" if soh >= 0.80 else "待退役"

读法:A if 条件 else B —— 条件真取 A,否则取 B。等价的 if/else 要写 4 行。

什么时候用什么时候别用
一个简单的二选一赋值需要 elif、或嵌套超过一层
放进 f-string / 列表推导式里逻辑有副作用(调用函数、改数据)

嵌套三元是"可读性杀手":

python
status = "待退役" if soh < 0.80 else ("预警" if soh < 0.85 else "健康")   # ⚠ 勉强能读

demo 里特意说了:"嵌套太多层就别用了,回到 if/elif/else —— 可读性优先。"

8.7 match:多分支时的清爽写法(Python 3.10+) ​

python
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}")

输出:

text
  状态码 0 → 正常
  状态码 1 → 预警
  状态码 2 → 待退役
  状态码 9 → 未知状态码

match 适合"一个值对多个固定选项"(状态码、命令、枚举):

if/elif/elsematch/case
判断范围任何条件(<、in、and)主要对值 / 结构做匹配
兜底elsecase _:
版本所有版本Python 3.10+

本书的阈值判断仍然用 if/elif/else,因为那些条件是范围判断(<、>=),不是"值等于什么"。 注意 case _ 不能少,否则没有匹配的值会什么都不做(不报错),程序悄悄跳过 —— 又一个静默 bug。

8.8 缩进就是语法 ​

python
soh = 0.78
if soh < 0.80:
print("待退役")        # ❌ IndentationError: expected an indented block

Python 用缩进表示"谁属于谁",这是它和其他语言最大的外观差异。

错误报错后果
该缩进没缩进IndentationError程序直接停
Tab 和空格混用TabError程序直接停
多缩进/少缩进一层不报错逻辑跑到了 if 外面,结果错

第三种最危险:

python
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 完整示例与输出 ​

bash
cd code
python demos/ch08-condition.py

真实输出(节选):

text
=== ① 二档:是否到退役线 ===
  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 前面,"待退役"永远不可达,且不报错。

坑 ②:= 写成了 ==,或反过来。

python
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 优先级搞混。

python
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):

python
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 位),并统计每档各几支。

期望输出:

text
SOH 1.000  → 健康
SOH 0.920  → 健康
SOH 0.884  → 健康
SOH 0.868  → 健康
SOH 0.848  → 预警
SOH 0.780  → 待退役

(校验){'健康': 4, '预警': 1, '待退役': 1}
参考答案
python
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}")

要点解释:

  1. 分支顺序是本题唯一真正的考点。先判最严格的 < 0.80,再判 < 0.85。 写反了 0.780 会被标成"预警",而且代码不报错。
  2. {soh:<6.3f} 里 <6 只影响显示宽度,不改变数值,也不影响比较。
  3. counts[status] += 1 用字典计数(第 09 章):注意它在循环外面初始化 —— 放里面每次都会被清零。
  4. 边界要确认:0.85 本身走 else(健康),0.80 本身走"预警"。

完整练习在 exercises/ex04-08-condition。

练习 2:找出下面这段代码的 bug
python
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)}")

要求:不要运行,先说出每支电芯的输出,再指出代码的问题,最后给出修正版。

参考答案

实际输出:

text
SOH 0.78 → 预警
SOH 0.84 → 预警
SOH 0.95 → 健康

问题:0.78 应该是「待退役」,却得到「预警」。 因为 0.78 < 0.85 成立,第一个分支就把它接走了,elif soh < 0.80 永远不可达。

这类 bug 的三个特点:

特点说明
不报错语法完全合法,程序正常跑完
不总是错0.84、0.95 的结果都是对的,只有"更严重"的那档出问题
方向单一错误总是把"严重"的样本标成"较轻"的档 —— 预警/筛查场景里后果很大

修正:把条件按"从严格到宽松"重排:

python
def classify(soh):
    if soh < 0.80:            # 最严格的最先判
        return "待退役"
    elif soh < 0.85:
        return "预警"
    else:
        return "健康"

修正后输出:0.78 → 待退役、0.84 → 预警、0.95 → 健康。

怎么预防:写完就把每一档的区间写成注释,或者干脆用"区间判断"而不是"阈值判断":

python
if 0.80 <= soh < 0.85:        # 链式比较,区间写得明明白白
    return "预警"
练习 3:预测输出(不要运行,先想)
python
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)

要求:先写下每一行的输出,再运行验证。不一致的地方,说出原因。

参考答案

预测输出:

text
True
False True False True
B
兜底
待退役
[]
True
False
表达式结果为什么
0.80 <= 0.84 <= 0.85True链式比较 = 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.0True== 比值:整数 1 和浮点 1.0 值相等
1 is 1.0Falseis 比对象身份;还会触发 SyntaxWarning,别这么写

三处最容易错的:

  1. bool("0") 是 True、bool([0]) 是 True —— 判断依据是"空不空",不是"里面是不是 0"。 所以"从配置里读到的 "0""在 if 里会被当成真。
  2. and / or 返回操作数本身,不是 True / False —— 这也是 x = user_input or "默认值" 能工作的原因。
  3. caps and caps[0] > 0 不报错:短路求值救了它。写成 caps[0] > 0 and caps 就会先 caps[0] → IndexError。"先判空、再取值"的顺序不能颠倒。

面向 AI 开发:条件判断就是"给数据贴标签" ​

AI 项目的一半工作量在"判断"上:判断样本合不合法、判断该进哪个分支、 判断异常还是正常、判断该不该停止训练。

① 数据分档 / 贴标签(labeling)—— 本章的主线场景。

python
df["health"] = "健康"
df.loc[df["soh"] < 0.85, "health"] = "预警"
df.loc[df["soh"] < 0.80, "health"] = "待退役"

这是第 27 章"派生列"的写法 —— 三行的顺序决定了最终结果。 Pandas 里更安全的做法是 pd.cut:

python
df["health"] = pd.cut(df["soh"], bins=[0, 0.80, 0.85, 1.0],
                      labels=["待退役", "预警", "健康"], right=False)

pd.cut 的好处是"每一档的区间写在一处",从根上避免顺序写反。

② 布尔掩码(boolean mask)—— 条件判断的向量化版本。

python
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。 括号也不能省(& 优先级高于 >)。

③ 短路求值 = 防崩护栏。

python
if caps and caps[0] > 0:            # 先判空,再取值
if df is not None and len(df) > 0:  # 先判 None,再判长度

数据管道里到处是"可能为空 / 可能为 None"的中间结果, 短路求值让"先检查、再用"写在一行里,不必嵌套一层 if。

④ 训练里的条件判断:早停、保存最优模型、跳过坏批次。

python
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 这么点,难点全在业务规则怎么定、边界归哪档、顺序对不对。 把每一档的区间写下来、把顺序检查一遍,这两件事比背语法重要得多。


下一章:第 09 章 · 字典与集合:用字典描述一支电芯