一份两百多行的 Python 脚本,报错位置却永远停在最后一句:ValueError: invalid literal for int() with base 10: ''
我第一眼不太信是数据本身的问题。
继续往上翻,果然看到一段代码:读取 CSV、判断字段、计算金额、拼接结果、记录异常,全挤在一个 for 循环里。条件判断套了四层,循环中间还夹着三个 continue。
这种代码平时跑得好好的,一旦数据脏一点,排查起来特别费劲。
Python 的条件判断、循环和函数都不复杂。真正容易写乱的,是三样东西混在一起以后,没有边界。
拿一个常见的订单导入脚本来说。CSV 中包含订单号、用户编号、商品数量和单价,我们需要过滤无效数据,再算出订单金额。
很多人第一版会这么写:
for row in rows:
if row.get("order_no"):
if row.get("quantity"):
if row.get("price"):
quantity = int(row["quantity"])
price = float(row["price"])
if quantity > 0and price > 0:
total = quantity * price
results.append({
"order_no": row["order_no"],
"total": total
})
代码能运行,但我一般不会让它进正式脚本。
最大的问题不是缩进难看,而是失败原因被吞了。订单号为空、数量格式错误、价格小于零,最后都是“没有进入 results”。等业务问某条订单为什么没导入,只能重新跑一遍,再临时加打印。
条件判断最好先处理失败分支。
defcheck_order(row):
order_no = row.get("order_no", "").strip()
ifnot order_no:
returnFalse, "订单号为空"
try:
quantity = int(row.get("quantity", ""))
price = float(row.get("price", ""))
except (TypeError, ValueError):
returnFalse, "数量或单价格式错误"
if quantity <= 0:
returnFalse, "商品数量必须大于0"
if price <= 0:
returnFalse, "商品单价必须大于0"
returnTrue, {
"order_no": order_no,
"quantity": quantity,
"price": price
}
这里没有什么炫技,就是把错误尽早返回。
我不太喜欢为了追求“只有一个 return”,硬把所有分支包在一个巨大的 if 里面。业务校验不是数学证明,哪个条件不满足,就在哪停下来,后面的人看代码也省事。
循环负责调度,不负责处理全部业务。
defload_orders(rows):
passed = []
rejected = []
for line_no, row in enumerate(rows, start=2):
ok, detail = check_order(row)
ifnot ok:
rejected.append({
"line": line_no,
"order_no": row.get("order_no", ""),
"reason": detail
})
continue
detail["amount"] = round(
detail["quantity"] * detail["price"], 2
)
passed.append(detail)
return passed, rejected
continue 在这里很合适。当前数据不合法,记录原因,然后处理下一条。
但 continue 也不能乱用。一个循环里跳转太多,执行路径会变得很碎。尤其是 continue 前面如果还有计数、文件写入或者状态更新,很容易漏执行。
我处理循环时通常会看两件事:这次循环的目标是什么,什么情况下应该立刻结束。
比如在一批订单里查找第一个金额异常的订单,找到后就没必要继续扫:
deffind_first_abnormal(orders, limit):
for order in orders:
if order["amount"] > limit:
return order
returnNone
这里直接 return 比设置一个变量再 break 更干净。函数已经拿到结果,就结束。没有必要让代码绕一圈再出去。
另一种情况是批量处理,单条失败不能影响后面的数据。这时异常要放在循环内部,而不是包住整个循环:
defcalculate_amounts(orders):
completed = []
failed = []
for order in orders:
try:
amount = order["quantity"] * order["price"]
completed.append({
**order,
"amount": round(amount, 2)
})
except (KeyError, TypeError) as exc:
failed.append({
"order_no": order.get("order_no", ""),
"error": str(exc)
})
return completed, failed
如果把 try 写在 for 外面,一条数据出错,整批任务都停了。这种写法在测试数据里不明显,上线跑导入任务时很烦,前面处理了多少、后面还剩多少,全得重新确认。
函数也不是拆得越细越好。
我见过一个字段校验被拆成七八个函数:判断是否为空一个函数,转整数一个函数,判断正数又一个函数。调用链看着很“规范”,排查时得来回跳文件。
函数值不值得拆,我一般看它有没有独立职责。
check_order() 负责校验并转换数据,load_orders() 负责遍历和收集结果,这种边界比较自然。至于一行 quantity > 0,没必要专门包装成函数。
最后把脚本入口收一下:
defrun_import(rows):
valid_orders, invalid_rows = load_orders(rows)
print(
f"导入完成,成功 {len(valid_orders)} 条,"
f"失败 {len(invalid_rows)} 条"
)
for item in invalid_rows[:10]:
print(
f"第 {item['line']} 行失败:"
f"{item['reason']}"
)
return valid_orders
条件判断决定当前数据能不能继续,循环决定下一条数据什么时候开始,函数负责把不同职责隔开。
三样东西单独看都简单,混在一起就很考验代码习惯。
遇到判断套循环、循环里又塞异常处理的代码,我一般不急着优化语法。先把失败分支拉出来,再把单条处理从循环里拿出去。代码层级一降,很多隐藏问题自己就露出来了。