KeyError: 'user_id'
没有文件名,没有数据行号,也不知道是哪条脏数据。开发机上重新跑一遍,又正常。
这种代码我现在看到,第一反应不是修 KeyError,而是先嫌弃:程序写得再快,出了问题只能靠猜,等于没写完。
写 Python 五年后再回头看,真正拉开差距的,不是背了多少语法,也不是会不会用某个热门框架。下面这三个技能,越早花时间,后面返工越少。
让代码自己留下证据
刚写 Python 时,我也爱到处 print()。
print("开始处理")
print(data)
print("处理失败")
本地看着热闹,一上定时任务就不行了。几百个文件同时处理,日志混在一起,根本不知道哪条信息属于哪个任务。
后来我写批处理,入口处一定先把上下文绑上。文件名、批次号、数据位置,能带的都带上。
import logging
from pathlib import Path
logger = logging.getLogger("bill_import")
defload_bill(file_path: Path, batch_no: str) -> list[dict]:
rows: list[dict] = []
with file_path.open("r", encoding="utf-8") as stream:
for line_no, raw_line in enumerate(stream, start=1):
try:
bill_no, amount = raw_line.rstrip().split("|")
rows.append({
"bill_no": bill_no,
"amount": int(amount),
})
except (ValueError, TypeError):
logger.exception(
"账单行解析失败 batch=%s file=%s line=%d raw=%r",
batch_no,
file_path.name,
line_no,
raw_line[:120],
)
return rows
这段代码没什么花活,但真出问题时,可以直接定位到批次、文件和行号。
异常也别随手吞。
try:
sync_order(order)
except Exception:
returnFalse
这种写法最省代码,也最费排查时间。至少把异常类型、业务主键和当前动作留下来。线上故障不怕报错,怕的是报错之后没有痕迹。
日志、指标、异常上下文,这些东西平时感觉多余,出一次问题就知道值钱了。
学会控制数据,而不是一把塞进内存
Python 写数据处理太顺手了,顺手到很多人上来就是:
records = file.read().splitlines()
result = [clean(item) for item in records]
文件小时没问题。文件一大,内存先替代码扛不住。
五年里我越来越依赖生成器,不是因为它“高级”,而是它能逼着代码按流处理。数据来一条,处理一条,不需要先把整个世界装进列表。
from collections.abc import Iterator
from pathlib import Path
defiter_device_codes(file_path: Path) -> Iterator[str]:
with file_path.open("r", encoding="utf-8") as stream:
for raw_line in stream:
code = raw_line.strip().upper()
ifnot code or code.startswith("#"):
continue
if len(code) < 8:
continue
yield code
defcollect_batches(
codes: Iterator[str],
batch_size: int = 300,
) -> Iterator[list[str]]:
bucket: list[str] = []
for code in codes:
bucket.append(code)
if len(bucket) == batch_size:
yield bucket
bucket = []
if bucket:
yield bucket
调用时也很直接:
for batch in collect_batches(iter_device_codes(Path("devices.txt"))):
push_to_api(batch)
这里值得投资的不是记住 yield,而是建立一种处理习惯:不要默认数据量永远很小,不要默认接口一次能吃完,也不要默认中间结果必须全部留在内存里。
生成器、上下文管理器、pathlib、collections 这些标准库,我现在的使用频率远高于很多第三方包。
框架会换,标准库里的基本功没那么容易过期。
把边界写清楚
Python 最容易把人带进一个坑:反正动态类型,先跑起来再说。
于是一个函数里的 payload,有时是字典,有时是 None,偶尔还是字符串。调用方只能靠运气。
这种代码短期写得快,后面改一次要翻半个项目。我现在更愿意在入口处多写几行,把数据边界卡死。
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
@dataclass(frozen=True)
classRefundRequest:
order_no: str
amount: Decimal
defparse_refund(payload: dict[str, object]) -> RefundRequest:
order_no = str(payload.get("order_no", "")).strip()
raw_amount = payload.get("amount")
ifnot order_no:
raise ValueError("order_no 不能为空")
try:
amount = Decimal(str(raw_amount))
except (InvalidOperation, ValueError):
raise ValueError(f"退款金额非法: {raw_amount!r}") fromNone
if amount <= 0:
raise ValueError("退款金额必须大于 0")
return RefundRequest(order_no=order_no, amount=amount)
再给关键边界补两个测试:
import pytest
deftest_parse_refund_rejects_empty_order_no() -> None:
with pytest.raises(ValueError, match="order_no"):
parse_refund({"order_no": " ", "amount": "12.50"})
deftest_parse_refund_keeps_decimal_precision() -> None:
request = parse_refund({
"order_no": "R2026008",
"amount": "19.90",
})
assert request.amount == Decimal("19.90")
类型标注、数据模型、边界校验、自动化测试,放在一起才有意义。只写类型标注,不检查输入,还是会在运行时炸;只写测试,不收紧函数职责,测试也会越来越难维护。
我现在判断一段 Python 代码靠不靠谱,通常不会先看它用了什么技巧,而是看三个地方:出错后能不能查,数据变大后会不会垮,输入失控时能不能及时拦住。
语法熟练当然重要,但半年基本就够用了。后面几年真正值得反复投资的,是可观察性、数据处理方式和工程边界。
这些东西不太适合拿来炫技。
但凌晨处理故障时,能少翻几十分钟日志。