Python 业务规则:条件、循环、容器与异常
今天从哪里开始
昨天我带大家输入数量计算鼠标金额,输入 0 或负数也能得到金额。今天先给自己的 D01 脚本补上这道业务判断,再处理运营提供的多条补录记录。稍后用已有订单的取消请求练习“先检查、后修改”。先不写循环,请大家逐条说出应该接受还是拒绝,以及失败后原订单能不能改变。
我们先把一条记录的判断顺序写清,再用字典和列表组织多条记录,最后放进循环。这样批量处理只是重复已经讲清的规则,错误记录也有自己的去处。
今天要学会什么
- 把数量合法性写成条件,区分真值与业务规则
- 用 try/except/else/finally/raise 分开成功、拒绝和收尾
- 用列表、字典、元组与集合组织商品和记录
- 用循环处理批次,核对成功、失败和总额
课堂结束时,用自己的项目检查:
- 能做出来:调整一条订单状态规则并测试中间坏记录,并展示正常输入的结果。
- 能查出问题:针对“漏掉记录、失败后修改订单、共享列表串线”中的风险,说明输入、预期与实际差异;失败后核对数据没有被误改。
- 能解释修改:提交代码、测试和修改前后结果,讲清自己接受或拒绝了哪一处 AI 建议。
先把自然语言规则写成判定顺序
今天只处理 W01 冻结范围:
| 业务问题 | 合同 | 失败后必须怎样 |
|---|---|---|
| 数量能不能读懂 | 输入必须能转换为整数 | 明确指出格式错误 |
| 数量能不能下单 | quantity >= 1 | 不计算金额,不改状态 |
| 商品是否存在 | 必须来自 W01 Fixture(固定测试数据或测试装置) | 不猜价格,不创建订单 |
| 当前状态能否执行动作 | 只允许冻结状态迁移 | 保留原状态 |
| 金额怎样计算 | Decimal 单价元 × 数量 | 与 W01-D01 手算一致 |
把顺序打乱会发生什么?
- 先算金额再查商品:未知商品可能被套上错误价格;
- 先改状态再校验:用户看到失败,程序里的状态却已经变了;
- 先写一个大
try:程序缺陷可能被伪装成“输入错误”; - 只写
if quantity:-3也会被当成真值。
读图时不要只走上方成功链,也要关注红色旁路,你要思考:输入是什么、在哪个条件第一次分歧、状态前后各是什么。
比较表达式得到 bool
规则表里的“至少一件”“商品存在”还只是文字。先把每个条件变成能得到真或假的表达式,随后才决定走哪条分支。

今天判断数量会用到 int / str / bool,订单集合用 list / dict;稍后比较 tuple / set 在状态与去重中的用途。float 只用于识别金额反例,complex 不用于本项目。先看清“字符串数字”和整数不是同一类型,再进入比较。
比较不会直接“执行支付”,它只得到 True 或 False:
quantity = 2
print(quantity >= 1)
print(quantity > 0)
print(0 < quantity < 10) # 独立语法实验,不是 W01 业务上限
print(quantity == 0)
print(quantity != 0)先预测,再运行。特别留意:
=是赋值,==才是相等比较;- 字符串比较按字符规则,不是按整数大小;
- 链式比较适合表达区间,但要写清上下界是否包含;
- 当前合同只有下界,至少检查
-1、0、1;不要凭空增加课程合同里没有的数量上限。
面试官如果问“为什么只测 1 还不够”,要能回答:它只能证明合法边界;0 和 -1 才验证下界外的输入是否被挡住。若企业以后增加上限,再把上限内外两侧加入测试。
and、or、not 与真值表
quantity_ok = True
product_exists = False
print(quantity_ok and product_exists)
print(quantity_ok or product_exists)
print(not product_exists)and 适合“所有前置条件都满足”,or 适合“多个合法选项中至少命中一个”。业务条件写成代码前,先把每一侧翻译回自然语言。
下面是高频错误:
action = "pay"
# 错误候选:右边的 "cancel" 没有参与比较
if action == "pay" or "cancel":
print("进入动作分支")请分别把 action 改为 pay、cancel、refund。如果三个值都进入分支,说明这段代码只“看起来像”表达了两个比较。
正确思路是每一侧都成为完整判断:
action == "pay" or action == "cancel"或者在规则稳定时写成成员判断:
action in {"pay", "cancel"}这是集合字面量,表示一组不重复的允许值。稍后我会把它与列表、元组一起比较;此处也可以先用完整的 or 比较。
短路求值:为什么条件顺序会影响安全
and 和 or 不一定计算右侧。左侧已经决定最终结果时,右侧会被跳过。
raw_quantity = "二"
is_digit = raw_quantity.isdigit()
print(is_digit and int(raw_quantity) >= 1)当左侧为 False,右侧 int(raw_quantity) 不执行,因此这条最小实验不会抛出 ValueError。但不要把所有输入问题都靠一条超长条件解决:一旦出错,很难知道格式、范围还是业务规则出了问题。
课程主线仍采用分阶段变量:
文本 → 转换 → 范围 → 资源 → 状态 → 动作短路是理解条件执行顺序的机制,不是把可读规则压成一行的理由。
if / elif / else 是按顺序选择路径
quantity = 0
if quantity < 0:
print("负数")
elif quantity == 0:
print("零")
else:
print("正数")第一条命中后,后续互斥分支不再执行。于是分支顺序要从更具体或更高优先级的条件开始。
现场故障:宽条件吞掉窄条件
quantity = 0
if quantity >= 0:
print("可以下单")
elif quantity == 0:
print("数量不能为 0")0 >= 0 已经为真,第二条永远没有机会。先别改代码,在纸上走一遍每条判断的真假,再提出最小修复。
输入转换和异常边界
input() 永远先得到字符串:
raw_quantity = input("请输入购买数量:").strip()
print(type(raw_quantity))当业务需要整数时才转换:
quantity = int(raw_quantity)输入 二 会得到 ValueError。这是一类可预期的用户输入失败,可以在转换边界处理:
try:
quantity = int(raw_quantity)
except ValueError:
print("数量必须是整数")这里最容易犯两个错误:
- 捕获范围太大,把
NameError、TypeError等程序缺陷也伪装成用户输入问题; - 捕获后继续往下跑,导致
quantity没有有效值却进入业务分支。
try/except 不是为了让错误消失。开发阶段若出现预期之外的异常,仍按 W01-D01 的顺序读:最后一行错误类型 → 文件/行号 → 最小原因假设 → 单点修复 → 原输入与变式复验。
不要写这个“万能兜底”:
try:
# 一大段订单逻辑
...
except Exception:
print("输入错误")它可能吞掉拼写错误、未定义名称甚至状态更新缺陷。先用条件分支识别业务拒绝;下一小节再把拒绝交给单条处理边界。我们只捕获已约定的 ValueError,拼写或类型程序错误仍需暴露。
一条记录结束后:raise、else、finally 各管一段
补录员需要知道“这条为什么没通过”,也需要核对处理总数。raise 主动发出异常;except 处理匹配的异常;else 只在 try 正常结束时执行;finally 在离开 try 结构时执行收尾。这里收尾只计数,不写订单金额。
我先运行一条输入,观察四段执行顺序,再把同一结构放进后面的批量循环:
raw_quantity = "0" # 分别换成 "2"、"二"
try:
quantity = int(raw_quantity)
if quantity < 1:
raise ValueError("数量至少为1")
except ValueError as error:
print("拒绝:", error)
else:
print("允许计算:", quantity)
finally:
print("本条检查结束")输入 2 走成功与收尾;输入 0 是主动拒绝;输入“二”是转换失败。else 中新抛出的异常不会被前面的 except 捕获。finally 不能当作支付成功回调,也不在其中 return 来覆盖原结果。以后商城文件、连接清理会继续使用这项机制。
**练习:**在自己的 D01 脚本中先保留单次输入,补齐二、0、-1、1四条路径。失败路径不显示成功金额;拼错变量名仍应出现可定位的 Traceback。完成后才转入批量。
真值不等于业务合法
Python 会把许多对象放进真假判断:
print(bool(0))
print(bool(-3))
print(bool(""))
print(bool("0"))
print(bool([]))
print(bool([0]))if quantity: 只能问“它是否为真值”,不能表达“数量是不小于 1 的整数”。if product_id: 只能挡住空字符串,挡不住不存在的 P9999。
企业业务代码要写业务规则本身,不把 Python 的真值规则误当成订单合同。
为什么 bool 要与数量整数区分?
Python 中 bool 是 int 的子类,isinstance(True,int) 为真。业务数量只接受真正正整数时,要用 type(value) is int 并检查大于零,不能让 True 代表购买一件。
列表:保存有顺序的一组记录
一条记录的判断已经讲清。现在回到运营的整批输入:先解决怎样保存多条记录,再让同一套校验逐条执行,不复制三份条件代码。

图中用生活中的容器帮助建立第一印象;在今天的订单程序里,容器不是一个抽象口号,而是保存订单 ID、订单字典和处理结果的数据结构。接下来要继续追问:是否有顺序、怎样定位、能否修改、字段缺失时怎样处理。
order_ids = ["O1001", "O-BAD", "O1002"]
print(len(order_ids))
print(order_ids[0])
print(order_ids[-1])列表索引从 0 开始。order_ids[3] 在只有三项时会抛出 IndexError。先读错误,再修正索引;不要用大范围异常把越界掩盖掉。
常用的最小操作:
success_ids = []
success_ids.append("O1001")
print("O1001" in success_ids)append 改变原列表。它没有返回“新列表”:
result = success_ids.append("O1002")
print(result) # None把 append() 的返回值重新赋给列表,是课堂最常见的容器错误之一。
字典:用键表达一条业务记录
order = {
"order_id": "O1001",
"order_status": "PENDING_PAYMENT",
"items": [
{"product_id": "P1001", "quantity": 2}
],
}字典的键承载字段含义。读取时先确认你拿到的是哪个层级:
print(order["order_id"])
print(order["items"][0]["product_id"])直接读取不存在的键会得到 KeyError。get() 可以提供缺失时的返回值:
print(order.get("customer_note"))但 get() 不是“自动修复合同”。核心字段缺失时,如果悄悄得到 None 继续计算,错误只会推迟。先判断这个字段是可选信息,还是缺失就必须拒绝的业务字段。
把商品、数量、状态组成一条记录,再把三条记录放在一起,你会选字典还是列表?
字典用字段名表达一条记录,列表保存有顺序的多条记录;循环逐条读取并分开保存结果,避免用同一个可变字典反复覆盖。
同一批数据,四种容器各适合什么
列表保留批次顺序,字典按商品编号找价格;元组适合表达固定允许状态,集合适合成员判断与去重。选择容器是为了让商城的查询和修改意图清楚。
allowed_states = ("PENDING_PAYMENT", "PAID", "CANCELLED")
seen_ids = set()
for order_id in ["O1", "O1", "O2"]:
if order_id in seen_ids:
print("重复:", order_id)
else:
seen_ids.add(order_id)
print(len(seen_ids)) # 2,集合不保证业务显示顺序
print(allowed_states[0])元组不能原地替换元素;集合不提供列表式下标。取最近两条补录记录可用 order_ids[-2:] 切片,切片不会修改原列表。推导式是已有循环的简写,不用它隐藏状态更新。现在把一组已支付编号去重,解释为什么去重后的集合不能直接充当有序批次。
状态迁移:先检查,再一次更新
状态动作由“当前状态 + 请求动作”共同决定。今天只允许:
| 当前状态 | 动作 | 结果 |
|---|---|---|
PENDING_PAYMENT | pay | 更新为 PAID |
PENDING_PAYMENT | cancel | 更新为 CANCELLED |
PAID | cancel | 拒绝,仍为 PAID |
CANCELLED | pay | 拒绝,仍为 CANCELLED |
下面是故意先改状态、后校验的错误片段,依赖一笔已有订单和保存下来的 previous_status(原状态)。已经知道字典怎样读取和修改字段,现在重点观察校验与更新的先后顺序:
order["order_status"] = "CANCELLED"
if previous_status == "PAID":
print("已支付订单不能取消")用户得到失败提示,但状态已经写坏。这叫半更新。正确顺序是:
- 保存并展示状态前值;
- 判断动作是否允许;
- 失败就结束本路径;
- 只有成功路径更新一次;
- 展示状态后值并与预期比较。
为什么订单能否取消,不能只看客户提交了 cancel 这个动作?
还要检查当前状态和业务规则;本课已支付订单不能沿用待支付取消路径。先判断全部条件,再更新状态,拒绝时保留原状态。
容器可变:两个名称可能指向同一个对象
先预测输出:
original = ["O1001"]
alias = original
alias.append("O1002")
print(original)alias = original 没有复制列表,只是让两个名称指向同一个对象。通过任一名称修改,另一边也能看见。
今天不展开深拷贝体系,但必须建立一个排错习惯:当列表或字典“自己变了”,检查是否有多个名称共享同一个可变对象。
两个变量指向同一个列表会发生什么?
赋值通常复制引用;一处 append,另一处也能看见。循环处理每笔订单时重新建立该笔容器,避免上一笔明细串入下一笔;浅复制仍可能共享嵌套对象。
for 循环:按顺序取出每个元素

图里遍历的是姓名列表,课堂代码把同一机制迁移到订单列表。不要照抄变量名;要说清每轮变量拿到什么、循环体重复什么、输出顺序为什么与列表顺序一致。
for order_id in order_ids:
print(order_id)每轮开始,循环变量 order_id 指向下一项。循环体缩进决定哪些语句每轮重复。
需要轮次时,可以使用 enumerate:
for round_no, order_id in enumerate(order_ids, start=1):
print(round_no, order_id)range(3) 会产生 0、1、2,适合明确次数;订单批处理更适合直接遍历记录,不要为了“用索引”写 range(len(orders)) 后再到处取值,除非索引本身就是任务需要。
while 在条件为真时重复,适合不知道会输入几次的纠错入口。先运行最小例:
raw_quantity = input("数量,q退出:").strip()
while raw_quantity != "q":
print("待校验输入:", raw_quantity)
raw_quantity = input("数量,q退出:").strip()循环末尾重新输入让条件有机会变化,输入 q 才退出。今天批次用 for;D05 把已学 while 接到菜单。
循环的四个观察点
每个循环至少能回答:
- 循环前初始化了什么?
- 本轮拿到哪条记录?
- 通过哪些条件后才更新累计和结果?
- 本轮怎样结束,下一轮是否继续?
批处理状态应该在循环外初始化:
from decimal import Decimal
total_amount_yuan = Decimal("0.00")
success_ids = []
failure_records = []若把它们写在循环内,每轮都会重置;最终往往只剩最后一笔。
累计:读取旧值,再写入新值
from decimal import Decimal
total_amount_yuan = Decimal("0.00")
for line_total_yuan in [Decimal("258.00"), Decimal("299.00")]:
total_amount_yuan = total_amount_yuan + line_total_yuan+= 是常见简写:
total_amount_yuan += line_total_yuan两种写法都依赖“旧累计值”。因此你必须能画出每轮前后:
| 轮次 | 本轮金额 | 累计前 | 累计后 |
|---|---|---|---|
| 1 | 258.00 | 0 | 258.00 |
| 2 | 299.00 | 258.00 | 557.00 |
先校验后累计。如果坏记录已经加进去,再用减法“补救”,中间状态会更难验证,也容易在异常路径留下污染。
continue 与 break:业务策略不同

上面的电商图负责解释两种业务策略,这张语法图负责快速回忆语法作用范围。两张图要连着讲:先判断“当前订单失败后能不能继续处理下一笔”,再选择 continue 或 break,不能看见异常就随手写一个。
continue:跳过当前轮剩余语句,进入下一轮;break:立刻结束当前循环,后续订单不再处理。
哪一个更好,要看业务策略:
- 批量导入中,一条格式错误是否允许记录失败后继续?
- 数据文件整体结构损坏时,是否应该立即停止?
W01 固定任务采用“业务坏记录明确登记后继续”,这样能观察成功—失败—成功三轮。若你选择停止,也必须在编码前写清理由和预期,且不能把失败记录计入成功累计。
一批订单中有一条填错,怎样让运营知道哪些成功、哪些需要补录?
按本课逐条处理合同,记录每条结果和失败原因,其余合法记录继续;不能只报整批成功,也不能把坏记录静默丢弃。
一条坏记录应该让整批停止吗?
由业务合同决定。今天练习要求逐条校验并收集失败记录,其余记录继续;异常处理放在单条边界,不能用一个宽泛 except 把整个批次静默吞掉。
筛选:只把满足条件的记录加入结果
pending_order_ids = []
for order in orders:
if order["order_status"] == "PENDING_PAYMENT":
pending_order_ids.append(order["order_id"])筛选不修改订单状态。若一个“查询待支付订单”的循环顺手把状态改成 PAID,说明读取和动作职责混在了一起。
今天先用显式循环,把每轮判断和结果变化看清。列表推导式留作阅读拓展,不作为核心提交:
pending_order_ids = [
order["order_id"]
for order in orders
if order["order_status"] == "PENDING_PAYMENT"
]如果解释不了推导式的遍历、条件和产出,就保留显式循环。
查询:找到就停,找不到也要可观察
target_id = "O1002"
found_order = None
for order in orders:
if order["order_id"] == target_id:
found_order = order
break循环结束后再判断 found_order。不能把“找到后退出”与“遇到第一条不匹配就报不存在”混在一起。
一个典型坏写法:
for order in orders:
if order["order_id"] == target_id:
print("找到")
else:
print("订单不存在")当第一条不匹配时它就打印“不存在”,即使第二条就是目标。用 target_id = "O1002" 逐轮跟踪,问题会立刻暴露。
把三种结果放回商城补录任务
我先在自己的 batch_orders.py 中准备商品价格字典和请求列表,再逐段增加处理。日包只给输入与 TODO,完成版由大家从昨天的脚本改出来。
| 输入记录 | 商品/数量文本 | 预期 |
|---|---|---|
| O1 | P1001 / 2 | 接受,258.00 |
| O2 | P1001 / 二 | 拒绝,保留编号和原因 |
| O3 | P1002 / 1 | 接受,299.00 |
| O4 | P1001 / 0 | 拒绝,不累计 |
| O5 | P9999 / 1 | 拒绝,未知商品 |
在循环外准备成功列表、失败列表、Decimal 总额与处理数;每条只在成功分支累计,finally 记录处理数。结果应为成功2、失败3、已处理5、金额557.00。空批次处理数0、金额0.00。本日只是本地规则校验,不向真实商城写订单或发起支付。
python batch_orders.py
python check_batch.py独立变化:把坏记录移到首尾,换一种合法数量,再核对“成功数+失败数=输入数”。取消规则仍单独用状态前后值验收,不能因批次金额正确就认定状态正确。
学生自己动手,AI 只能改你允许改的地方
我先保存当前可运行版本,再完成“调整一条订单状态规则并测试中间坏记录”。让 AI 一次只修改负责该行为的文件;修改前预测正常与失败结果,检查差异,再运行回归。每人需要解释一处接受或拒绝的建议。
错误与边界怎样验收
今天重点检查:漏掉记录、失败后修改订单、共享列表串线。保留输入、预期、实际输出和修复后的结果,不能只贴全绿截图。未验证的外部依赖如实记为待验证。
留给下一阶段
请把三条记录按“成功—失败—成功”重新跑一遍:失败记录留下原因,下一条仍能处理,累计金额没有混入失败项。代码开始变长、规则开始重复,明天就从这个维护问题引入函数和模块。