周日晚上 11 点的消息
某私募的中后台周日晚上甩来一条消息:风控组明早要用你写的 summarise.py 跑一份沪深300成分股的 tick 滚动 VWAP,他们那台服务器装的是干净的 Python 3.11、没装你电脑上的任何包。你抓起脚本一看,它现在还是 notebook 里那个用 print 打日志、入口写在最后一格、依赖装在 ~/anaconda3/ 里的样子。要把它变成一个能在别人机器上跑、出错时有 traceback、被 CI 拒之门外都能定位到哪一行的命令行工具,周一早 9 点的 T+1 报表才能按时出。这一课要补齐的就是这四件:把 print 换成 logging、把入口写成 argparse、把依赖锁进 venv、再让 ruff / black / mypy 在提交前帮你过一遍。
为什么不用 print
print 在 notebook 里是探针,在 .py 文件里是技术债。两类典型故障值得记住:
- 生产环境留印:你把
print("loading", path)留在了打包到 Airflow 的代码里,DAG 一天跑 600 次,stdout 写满 200MB,运维半夜来电话;想关掉,得改源文件再发布一次——logging只要把 level 从INFO调到WARNING就静音。 - stdout 被吞:某些任务调度器把 stdout 重定向到
/dev/null,pytest 默认捕获 stdout,你以为打了日志,实则一行都没留;logging有自己的 handler 链路,行为可预测。
logging 的最小可用配置只有一段,写在你脚本的最顶上一次:
import logging; logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(name)s: %(message)s'); log = logging.getLogger(__name__)
三件事在做:装一个根 logger、规定每条日志的格式(时间戳—级别—logger 名—消息)、给当前模块拿一个名字叫 __name__ 的 logger。__name__ 在 summarise.py 里就是字符串 "__main__" 或 "summarise",这个模块被别处 import 时,所有日志会自动带上来源,不用你手写。
五个级别按严重程度递增,各对应一种「说话场合」:DEBUG(开发期细节,生产环境 silence)、INFO(正常流程的里程碑,例如「读取了多少行 tick 数据」)、WARNING(不正常但能继续,例如窗口被截断)、ERROR(这次任务失败)、CRITICAL(整套服务挂了)。一条经验法则:写在 except 子句里的,别用 log.error(str(exc))——用 log.exception("..."),它会自动把 traceback 接在消息后面,省得你再做一次 traceback.format_exc()。
argparse:给脚本装一个门
logging 解决了「说话」,argparse 解决了「听话」。目标是让风控组能这么调:
python tools/summarise.py ticks.csv --window 5 --verbose
ticks.csv 沿用上一课的列布局(code,price,volume),--window 控制滚动窗口大小,--verbose 把日志级别调到 DEBUG。骨架长这样,写完即用:
from argparse import ArgumentParser
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument("path")
parser.add_argument("--window", type=int, default=10)
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()
n = count_rows(args.path)
log.info('读取了 %d 行 tick 数据', n)
if args.window > n:
log.warning('窗口大于数据长度, 截断为 %d', n)
run(args.path, window=args.window, verbose=args.verbose)
四件你免费拿到的事:(1) path 是位置参数(positional argument),漏了会被告知;(2) --window 是可选项,自带类型转换与默认值;(3) --verbose 是布尔开关,出现即 True;(4) python tools/summarise.py --help 自动列出全部参数与默认值,不用你写一行文档。把入口收在 if __name__ == "__main__": 里,这个文件依旧可以被别的脚本 import 而不会自己跑起来。
venv:每个项目一个鞋盒
把上面这套交付给风控组之前,先把依赖锁进项目自己的虚拟环境(virtual environment, venv)。规则只有一条:每个项目一个 venv,不要往系统 Python 里 pip install 任何东西。三行命令:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Windows 上把第二行换成 .venv\Scripts\activate,其余不变。.venv 是目录名约定,加进 .gitignore;requirements.txt 是你把 pandas==2.2.3 这种行锁进 git 的清单,下游同事 clone 下来按同样三行就能重建一模一样的环境。这背后真正在解决的问题是依赖隔离(dependency isolation):你 A 项目用 numpy 1.26、B 项目用 numpy 2.0,系统 Python 同时装两个就会打架,venv 把它们分进不同沙盒。
国内学习者可以在 pip install 之前用 pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ 之类的命令切到国内镜像源,下载会快几个数量级——这是工程便利,不是官方推荐,选哪一个跟你公司在用的源对齐即可。uv(Astral 出品的新一代工具)能把 venv 创建和依赖安装压到原来的几十分之一时间,下一节模块 3.1.3「打包与测试」会把它讲完,这里你先知道有这么个替代。
CI 会跑的那四件
风控组的 repo 接了 CI,你的 PR 一推下面这四件会自动跑,事先在本地过一遍能少掉一半「红叉」:ruff (lint + import sort)、black (format)、mypy (static type check)、pytest (test runner — full treatment in 3.1.3)。展开来看:
ruff等价于过去的flake8+isort+ 一堆插件,速度快一个量级,常见错误它还能--fix自动修。black是 opinionated 格式化器(formatter),零配置,所有人 PR 风格一致,再不用为「逗号后是否空格」开评审会。mypy是静态类型检查器,把上一课你写的那些def parse_ticks(path: str) -> Iterator[Tick]类型注解真正验证一遍。pytest是测试运行器,这里只点一下名,完整使用方式在 3.1.3。
pyproject.toml 是这四件工具的统一配置文件,你现在只需要看一眼 ruff 的最小配置长什么样——别照搬全文,3.1.3 会教你从零写一个完整的 pyproject.toml:
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I"]
第一段把行长上限设到 100,第二段告诉 lint 启用三类规则:E 风格错误、F 真正的 bug(未定义名等)、I 导入排序。把这块加到 repo 根目录的 pyproject.toml,ruff check . 就会按你的口味跑。
练习
Exercise
将下列基于 print 的诊断改写为使用 logging 模块,并在调用点选择合适的级别。给定 print("loaded", n, "rows")、print("WARNING: missing column ticker")、print("ERROR:", exc); raise 三行,分别用模块顶上的 log = logging.getLogger(__name__) 改写为 INFO、WARNING、ERROR 三种级别的日志调用;第三条必须用 log.exception(...),以保留 traceback。
提示
%-style 占位符把变量塞给 logger,例如 log.info("loaded %d rows", n)——别先做 f-string 拼接,那会绕过 logger 的 lazy formatting,性能和过滤都受影响。提示
except Exception as exc: 块里:log.exception("failed to load") 即可——exception 等价于 error 但自动附 traceback,正文不必再带 exc,也不必再 raise 一次。衔接下一个模块
到这里,3.1.2「Python 惯用法与开发工具」就收尾了:你能给函数贴类型注解(第 1 课)、写出节流的生成器(第 2 课)、把横切关注点封装成装饰器(第 3 课)、把脚本交付成有日志、有命令行入口、能在干净环境里跑的命令行工具(本课)。下一个模块 3.1.3「打包与测试」把这条交付链补齐:怎么从零写一个 pyproject.toml、怎么用 pytest 与 fixture 把上面这套 summarise.py 的回归测试落进 CI、怎么把模块打成 wheel 让风控组的部署脚本一行 pip install 就能装上。今天写的这些日志和参数,下个模块会成为测试的输入与断言的对象。