模型训练脚本通常不是一条直线:需要先检查输入,再按条件选择策略,最后遍历批次并汇总结果。本节把这种“根据状态选择下一步”的思维翻译成 Python 语法。完成后,你应能写出清晰的分支、可终止的循环,并用推导式表达简单的数据转换。
把每个条件看成一个决策节点,把每次循环看成一次状态更新。先写清楚“何时进入、何时退出、每轮改变什么”,再选择 if、for 或 while,能显著减少死循环和遗漏分支。
if、elif、else 使用布尔表达式决定代码块是否执行。缩进是语法的一部分,同一分支中的语句必须保持一致缩进。
score = 72
if score >= 90:
result = "优秀"
elif score >= 60:
result = "及格"
else:
result = "需要复习"
print(result)
条件应尽量表达一个业务判断;复杂判断可以先拆成有名字的布尔变量:
has_data = len(samples) > 0
has_labels = len(labels) == len(samples)
ready = has_data and has_labels
条件分支可以组合比较、逻辑运算和成员判断。先写出“最具体的情况”,再写范围更大的情况,避免前面的条件把后面的分支遮住:
label = "cat"
confidence = 0.92
if label not in {"cat", "dog"}:
decision = "未知类别"
elif confidence < 0.5:
decision = "需要人工复核"
elif label == "cat":
decision = "猫"
else:
decision = "狗"
print(decision)
只有在分支内部还需要再次判断时才嵌套 if。如果嵌套超过两层,通常应该提取成函数或拆成具名布尔变量。简单的二选一可以使用条件表达式:
score = 0.73
status = "通过" if score >= 0.6 else "未通过"
条件表达式适合一个短结果,不要把多条语句或复杂副作用塞进一行。
if 与 elif 链多个独立的 if 都会分别判断;if/elif/else 是一条互斥选择链,命中一个分支后就不再检查其余分支:
score = 95
if score >= 60: # 独立的 if:会执行
print("及格")
if score >= 90: # 另一个独立的 if:也会执行
print("优秀")
if score >= 90: # elif 链:只执行第一个满足的分支
print("等级 A")
elif score >= 60:
print("等级 B")
else:
print("等级 C")
Python 用缩进表示代码块。推荐统一使用 4 个空格,elif 和 else 必须与 if 对齐;混用 Tab 和空格或少缩进一行,可能触发 IndentationError,也可能让语句落在错误分支中。
React Flow Animation
React Flow 管理节点与连线;切换下一步时,当前连线会流动。
取得分数,准备进入第一个条件判断。
这个动画只演示本节最容易混淆的 if / elif / else,高亮流程图节点代表当前执行位置;可以手动输入“分数”和“及格阈值”,再点击播放或逐步按钮:分数为 95 会走 if 的“优秀”路径,72 会走 elif 的“及格”路径,低于阈值则走 else。
空容器、0、None 会被当作假值;非空容器和非零数字通常是真值。and 与 or 会短路,因此可以安全地把前置检查放在前面:
items = ["cat", "dog"]
if items and items[0] == "cat": # items非空并且一项为"cat"
print("第一项是 cat")
用 is None 判断缺失值,不要把 is 当作普通相等比较:== 比较内容,is 比较是否为同一个对象。
写循环前先回答三个问题:初始状态是什么?每轮循环改变什么?什么条件会停止?这三个答案分别对应初始化、循环体和退出条件。
for:遍历已知序列for 适合遍历列表、字典、字符串或 range。循环体内只维护当前项和必要的累积状态:
losses = [0.9, 0.6, 0.4]
total = 0.0
# 遍历losses列表每一项,累加到total
for loss in losses:
total += loss
print(total / len(losses))
enumerate():
作用:遍历同时拿到下标+元素
语法:enumerate(可迭代对象,start=0),start为下标从几开始,默认为0
for idx, val in enumerate(nums, start=1):
print(idx, val)
"""
1 10
2 20
3 30
"""
zip():
作用:并行打包多个序列,按位置配对,短的序列决定最终长度(最短对齐)
语法:
a = [1,2,3]
b = ["x","y","z"]
for va, vb in zip(a,b):
print(va, vb)
"""
1 x
2 y
3 z
"""
需要索引时优先使用 enumerate,同时遍历两个序列时使用 zip:
for index, loss in enumerate(losses, start=1):
print(f"step={index}, loss={loss:.2f}")
遍历字典时,直接遍历得到的是键;需要键和值时使用 .items(),只需要值时使用 .values():
metrics = {"accuracy": 0.91, "recall": 0.84}
for name, value in metrics.items():
print(f"{name}: {value:.2f}")
range(stop) 生成从 0 到 stop - 1 的整数序列;range(start, stop, step) 可以指定起点和步长。zip 把多个序列按相同位置配对,并在最短的序列结束时停止,因此需要确认各序列长度是否应当一致。
for epoch in range(1, 4):
print("epoch", epoch)
names = ["train", "validation"]
counts = [120, 30]
for name, count in zip(names, counts):
print(f"{name}: {count} samples")
zip 不会因为长度不同自动报错,而是静默按短序列截断。需要严格一一对应时,先比较长度:
names = ["train", "validation"]
counts = [120]
print(list(zip(names, counts))) # [('train', 120)]
if len(names) != len(counts):
raise ValueError("names 和 counts 的长度必须一致")
Loop Flow Animation
滚轮可缩放画布;当前执行边会在步骤切换时流动。
循环开始前,先准备初始状态。
动画把 for 循环拆成“初始化、条件判断、循环体、更新状态、回到条件”的闭环。滚轮可缩放画布;播放或点击下一步时,当前连线会流动。break 边表示直接离开循环,条件为假则自然结束。
while:直到条件不再成立while 适合重试、读取队列或迭代未知次数的过程。循环体必须改变退出条件,并设置上限或超时,避免无限循环:
attempt = 0
while attempt < 3:
attempt += 1
print(f"第 {attempt} 次尝试")
处理“读到特殊值才结束”的任务时,可以使用哨兵值。下面的循环在读到 None 时停止,并且不会把哨兵当作数据处理:
batch_stream = [[1, 2], [3, 4], None]
index = 0
while True:
batch = batch_stream[index]
index += 1
if batch is None:
break
print("处理批次:", batch)
如果退出条件来自外部资源(文件、队列或网络),还要考虑异常和超时;在本地练习中至少设置最大尝试次数。
更常见的重试模式是“最多尝试几次,成功就提前停止”:
max_attempts = 3
for attempt in range(1, max_attempts + 1):
response_ok = attempt == 2 # 模拟第 2 次成功
if response_ok:
print(f"第 {attempt} 次成功")
break
else:
print("达到最大尝试次数,仍未成功")
break、continue 与 elsecontinue 跳过当前轮,break 立即离开循环。循环 else 只在循环自然结束、没有触发 break 时执行:
for value in [2, 4, 7, 8]:
if value % 2:
print("找到奇数", value)
break
else:
print("全部是偶数")
例如筛选训练样本时,continue 可以跳过缺失值,避免把无效样本加入结果:
scores = [0.8, None, 0.4, 0.9]
passed = []
for score in scores:
if score is None:
continue
if score >= 0.6:
passed.append(score)
print(passed) # [0.8, 0.9]
break 只结束它所在的最内层循环。多层循环需要一起退出时,优先把内层逻辑提取成函数,或使用清楚命名的标志变量。
列表推导式的基本结构是 [表达式 for 变量 in 序列 if 条件]。先写出等价的普通循环,再压缩为推导式,便于检查逻辑:
data = [1.2, -0.5, 3.4, -2.1, 0.8]
relu_output = [x if x > 0 else 0 for x in data]
print("ReLU 模拟滤波:", relu_output)
字典推导式适合构造键值映射,集合推导式会自动去重:
推导式也可以带筛选条件,筛选条件写在 for 后面;如果同时需要转换和筛选,先读成普通语言再写代码:
scores = [0.42, 0.81, 0.67, 0.39]
passed = [score for score in scores if score >= 0.6]
scaled = [round(score * 100) for score in passed]
print(passed, scaled) # [0.81, 0.67] [81, 67]
不要混淆两种条件位置:for 后的 if 会过滤元素;表达式中的 if ... else ... 会保留每个元素但转换它:
values = [-2, 0, 3]
positive_only = [x for x in values if x > 0]
relu_like = [x if x > 0 else 0 for x in values]
print(positive_only) # [3]
print(relu_like) # [0, 0, 3]
在 Python 3 中,推导式中的循环变量不会覆盖外层同名变量;普通 for 则会:
item = "outside"
_ = [item * 2 for item in [1, 2]]
print(item) # outside
多个 for 会形成嵌套循环。它适合很小的笛卡尔积,但数据量大时要警惕复杂度:
colors = ["red", "blue"]
sizes = ["S", "M"]
variants = [(color, size) for color in colors for size in sizes]
print(variants)
条件超过一层、表达式需要多行或包含副作用时,改回普通循环。可读性比少写几行代码更重要。
如果每个样本只处理一次,样本数翻倍,工作量大致也翻倍,记作 O(n)。两层循环都遍历同一批样本时,工作量大致变成四倍,记作 O(n^2)。这不是精确计时工具,而是提醒你在数据变大前检查循环结构。
samples = ["a", "b", "c"]
# O(n): 每个样本处理一次
for sample in samples:
print(sample)
# O(n^2): 每一对样本都要比较;这里只用于演示小数据
for left in samples:
for right in samples:
print(left, right)
后续使用 NumPy 和 pandas 时,优先寻找向量化、聚合或连接操作,而不是在大型数组和表格上手写嵌套 Python 循环。
运行下面的代码,修改 threshold,观察通过筛选的样本和均值如何变化:
threshold 改成 0.9,观察没有样本通过时为什么仍然不会除以 0。passed 的普通 for 循环改写成列表推导式,并比较哪一种更容易调试。None 分数,先用 continue 跳过它,再计算平均值。请独立完成这个流程:遍历 raw_scores = [0.8, None, 1.2, 0.4, -0.1];遇到 None 时用 continue 跳过;遇到不在 0 到 1 范围内的值时打印异常值并用 break 停止;最后用推导式把已经处理的合法分数转换成百分制。完成后思考:提前 break 与“跳过所有异常值”的策略分别适用于什么任务?
训练循环通常会在每个 batch 上完成“读取、检查、计算、记录”四步。下面是一个不依赖第三方库的简化版本,重点是观察控制流,而不是训练模型:
这里的外层 for 表示 epoch,内层 for 表示 batch,break 只会结束它所在的最内层循环。真实训练代码还会加入验证集、梯度更新和日志系统,但控制流骨架相同。
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 分支结果与预期不符 | 条件顺序错误,宽条件抢先命中 | 先写更具体的条件,再写兜底 else |
| 循环永不结束 | while 条件依赖的变量没有更新 | 在循环体中明确更新,并设置最大轮数 |
IndexError | 直接访问可能为空的列表 | 先判断容器是否为空,或使用 for 遍历 |
zip 后少了数据 | 两个序列长度不同,zip 按短序列截断 | 需要严格对应时,先用 len() 比较长度 |
IndentationError | 同一代码块的缩进不一致 | 统一使用 4 个空格,并让 elif/else 与 if 对齐 |
| 推导式难以阅读 | 嵌套条件或副作用过多 | 拆成普通循环和具名中间变量 |
| 字典遍历拿不到键和值 | 只遍历了键 | 使用 for key, value in mapping.items() |
if/elif/else 表达互斥的业务分支。for 或 while,并说明循环的退出条件。break、continue 和 enumerate。if 与 if/elif/else 链的执行差异。if、elif 和 else 的执行顺序是什么?while 循环最终会停止?break 与 continue 分别改变哪一层控制流?for x in values: if x > 0: result.append(x) 改写成列表推导式。[x for x in values if x > 0] 与 [x if x > 0 else 0 for x in values] 的结果为何不同?下一章:04.核心数据结构。你将把本节的遍历和分支应用到列表、字典、集合与嵌套数据上。