一个脚本里同时出现 os.path.join()、字典判空、手写参数解析,再塞上几段几乎一样的 try...finally,这种代码我第一眼就不太想改。不是不能跑。
是改一次容易漏三处,过两个月连作者自己都不敢动。
Python 标准库里有不少东西,作用不是让代码显得高级,而是把那些每天重复写、还特别容易写错的边角活直接收走。
1. pathlib:路径别再拿字符串硬拼
批量清理空日志时,没必要在 os.path 里来回套娃。
from pathlib import Path
log_dir = Path("/srv/order-service/logs")
for log_file in log_dir.glob("*.log"):
if log_file.stat().st_size == 0:
log_file.unlink()
路径拼接、文件判断、遍历、删除,全在同一种对象上完成。
我现在看到这种代码:
base_dir + "/" + date + "/" + filename
一般都会顺手改掉。Windows、Linux 路径分隔符不同只是一个问题,更麻烦的是字符串拼接根本表达不出“这是一个路径”。
2. collections:少写一堆字典初始化
按渠道归集失败订单,很多人会先判断 key 在不在。
from collections import Counter, defaultdict
orders_by_channel = defaultdict(list)
for row in import_rows:
orders_by_channel[row["channel"]].append(row["order_id"])
failed_reasons = Counter(
row["reason"]
for row in import_rows
if row["status"] == "failed"
)
defaultdict 省掉反复初始化列表,Counter 省掉手写计数。
这种逻辑看着简单,业务一多,代码里很快就全是:
if key notin result:
result[key] = []
没必要。
3. dataclasses:数据对象别再手写样板代码
导入任务的结果对象,经常只有几个字段,却要写初始化、比较、打印。
from dataclasses import dataclass, field
@dataclass(slots=True)
classImportResult:
filename: str
total: int = 0
success: int = 0
errors: list[str] = field(default_factory=list)
@property
deffailed(self) -> int:
return self.total - self.success
这里有个坑,列表默认值别直接写成 []。共享可变对象的问题,线上批处理脚本里我见过不止一次,排查时还很绕。
4. enum:状态值别散落在代码里
任务状态一旦到处写 "running"、"failed",迟早会有人拼成 "fail"。
from enum import Enum
classTaskState(str, Enum):
PENDING = "pending"
RUNNING = "running"
DONE = "done"
FAILED = "failed"
defcan_retry(state: TaskState) -> bool:
return state is TaskState.FAILED
枚举减少的不只是重复字符串,还把状态范围收紧了。代码补全能提示,重构也更踏实。
5. contextlib:清理动作统一放回出口
统计每个处理步骤的耗时,最烦的是每段代码都写一次开始时间和日志。
import logging
from contextlib import contextmanager
from time import perf_counter
logger = logging.getLogger(__name__)
@contextmanager
defrecord_cost(step: str):
started = perf_counter()
try:
yield
finally:
cost_ms = (perf_counter() - started) * 1000
logger.info("step=%s cost_ms=%.1f", step, cost_ms)
with record_cost("validate_rows"):
validate_rows(import_rows)
异常有没有抛出,耗时日志都会打出来。
资源释放、临时文件清理、事务收尾,也是同一个思路。别在三个分支里复制相同的关闭代码。
6. functools:重复计算直接缓存
规则文件在一个进程里可能被读取几十次,内容没变,就别反复解析。
import json
from functools import lru_cache
from pathlib import Path
@lru_cache(maxsize=16)
defload_rules(rule_file: str) -> dict:
path = Path(rule_file)
return json.loads(path.read_text(encoding="utf-8"))
lru_cache 适合输入确定、结果稳定的函数。
数据库查询、带当前时间的计算不要随便套。缓存代码不难写,缓存失效才难处理,这地方别贪。
7. itertools:列表处理别套三层循环
多个批次返回了一组订单号,需要合并去重:
from itertools import chain
batch_order_ids = [
["A1001", "A1002"],
["A1002", "A1003"],
["A1004"],
]
unique_ids = set(chain.from_iterable(batch_order_ids))
chain.from_iterable() 比手写嵌套循环更直接,也不用先制造一个巨大的中间列表。
标准库里这类迭代工具很多,但我不建议为了少两行代码硬用。看一眼说不清在干什么的写法,省下来的通常不是维护成本。
8. argparse:脚本参数别自己拆 sys.argv
线上补数据脚本最怕参数位置传错。
import argparse
parser = argparse.ArgumentParser(description="重跑指定日期的失败订单")
parser.add_argument("--date", required=True)
parser.add_argument("--limit", type=int, default=500)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
print(args.date, args.limit, args.dry_run)
参数校验、默认值、帮助信息都处理了。
比起手写:
date = sys.argv[1]
limit = int(sys.argv[2])
多不了几行,但脚本交给别人执行时,少出很多低级问题。
这些标准库不算冷门,也没有什么炫技成分。真正有用的地方,是把路径处理、状态约束、资源回收、参数校验这些重复劳动收拢到固定写法里。
业务代码已经够容易变了,边角代码就别再一遍遍手搓。