← 返回模块
3.1.3.4beta 可读 · 未来付费内容校验中内容版本 2026-05-27

构建、发布与版本号

3.1.3 · Python 打包与测试 · 编程

周三晚上 11 点,你在某 私募 量化组里维护着一个内部小包 xyzprice:封装 涨跌停 价计算、复权因子拼接、T+1 持仓核算这三件每周都要做的事。组里另外两位 PM 想直接 pip install xyzprice 就能用,而不是每次复制粘贴你脚本里那几个函数。pyproject.toml 已经写好,pytest 测试全绿——下一步只剩四件事:构建产物、推上 PyPI、写一份变更日志、再打一个 Git 标签把这一刻钉在历史上。

两种分发:sdist 与 wheel

Python 包有两种标准产物。​​源代码分发​​(sdist)是一个 .tar.gz,里面装着你的源代码与 pyproject.toml,下载到任何平台都能从头重新构建;​​wheel​ 是一个 .whl(本质是 zip),把 Python 代码加 *.dist-info/ 元数据目录预先打包好,安装时直接解压到 site-packages,无需任何构建步骤。

纯 Python 项目两份产物都是几秒钟的事;但只要包里带 C 扩展或 Rust 扩展,wheel 就是性能关键路径——它把编译这一步从用户机器挪到了你的 CI 上。本课全程只造 py3-none-any 的纯 Python wheel,跨平台编译的故事留给 3.5.4 与 3.6.5。

构建命令两行就够:

pip install build
python -m build

跑完后 dist/ 目录里会出现两个文件:dist/xyzprice-0.1.0.tar.gzdist/xyzprice-0.1.0-py3-none-any.whl。wheel 文件名按 PEP 427 拼接——包名、版本 0.1.0、Python 标签 py3(任意 Python 3 解释器都能跑)、ABI 标签 none(不依赖具体的 CPython ABI)、平台标签 any(任何 OS 与 CPU 架构)。三个 none / any 共同声明「这是一个纯 Python 包」。等价的更短一行是 uv build,产物同样落在 dist/

语义化版本:MAJOR.MINOR.PATCH

事实标准是 SemVer 2.0:MAJOR.MINOR.PATCH,规则只有三条:

  • MAJOR 加 1:你做了​​破坏性变更​​(breaking change),现有用户必须改自己的代码。
  • MINOR 加 1:你​​新增​​了向后兼容的功能(backward-compatible feature),老调用照样工作。
  • PATCH 加 1:你只是​​修了 bug​​(backward-compatible bug fix),行为更贴近文档承诺。

社区还有一条惯例:0.x.y 阶段(pre-1.0)「一切皆可变」,破坏性变更允许只升 MINOR;打出第一个 1.0.0 之后才严格执行三条规则。CalVer(按日期编号)与 ZeroVer(永远 0.x.y)是两种替代方案,但 Python 库的主流仍是 SemVer。

三个小 diff 选号试手:

  • 「修掉 涨跌停 价计算的 off-by-one」——行为更贴近文档、老调用不变 → ​PATCH​​。
  • 「为 科创板 代码(688 开头)加支持」——新增能力、老代码原样跑 → ​MINOR​​。
  • 「把公开函数 mean_price 重命名为 vwap」——任何老调用都会 AttributeError​MAJOR​​。

发布到 TestPyPI 与 PyPI

正式推 PyPI 之前,先在 TestPyPI(test.pypi.org)走一遍——它是 PyPI 的沙箱镜像,账号体系独立,可放心试错。

pip install twine
twine upload --repository testpypi dist/*
pip install -i https://test.pypi.org/simple/ xyzprice

第三条用 -i 强制 pip 走 TestPyPI 索引,验证别人确实能装上你的包。验证通过后,把 --repository testpypi 去掉重跑一次 twine upload,就是正式 PyPI 发布。PyPI 与 TestPyPI 都是全球公共服务,国内 可直接访问,只是上传速度受跨境链路影响——建议在稳定网络下完成 twine upload。企业内网常额外架一层私有索引(devpi、Artifactory、AWS CodeArtifact 等),具体接入留给 3.6.5。

PyPI 现在主推​​受信任发布者​​(trusted publisher):让 GitHub Actions 这样的 CI 平台用 OIDC 直接向 PyPI 证明「我就是 xyzprice 项目授权过的 workflow」,从此不必在 CI Secrets 里塞一条长期有效的 API token——后者一旦泄露就是全包接管。本课只点名这件事的存在;具体接线步骤在 3.6.5 配合 CI 一起讲。

变更日志与 Git 标签

每次发版前,把改动写进 CHANGELOG.md。社区事实标准是 Keep a Changelog,骨架如下:

# Changelog

## [Unreleased]

## [0.2.0] - 2026-05-24

### Added
- 科创板(688xxx)代码的 涨跌停 价计算支持

### Changed

### Fixed

### Removed

## [Unreleased] 永远在顶部,平时未发版的改动先堆在这里;切版本时把它下面的内容剪到一个新的 ## [0.2.0] - 2026-05-24 标题里就完成了一次发版记录。四个固定小节 Added / Changed / Fixed / Removed 覆盖了 99% 的改动语义。

随后给这一刻打 Git 标签:

git tag -a v0.2.0 -m "Release 0.2.0"
git push origin v0.2.0

-a 创建带说明的 annotated tag(而非轻量 lightweight tag),-m 写释义;push origin v0.2.0 把标签推到远端。约定上,Git 标签带前缀 vv0.2.0)、pyproject.toml 里的 version 字段不带(0.2.0),视觉上区分「这是一个 tag」还是「这是一个版本号」。

动态版本号的三种来源

pyproject.tomlversion 字段有三种常见写法:

  1. ​字面量​​:version = "0.1.0" 直接写死,最简单也最透明。
  2. ​从代码读​​:version = {attr = "xyzprice.__version__"} 加上 dynamic = ["version"],构建后端会去读源码里那一行 __version__,避免 toml 与代码不一致。
  3. ​从 Git 标签读​​:用 hatch-vcssetuptools-scm 插件,构建时读最近一个 v* 标签——好处是「打 tag 就等于发版」,代价是无 tag 的 dev 构建文件名会很丑。

对小项目,​​默认选第一种​​。等到流程长出 CI 自动化、需要单一事实来源(single source of truth)时再切到方案 2 或 3,复杂度与收益才匹配得上。配合 towncrier 这类自动变更日志生成器,是更后面的故事。

练习

Exercise

你维护一个版本号为 1.4.2 的包。对下面每一项改动,从 1.4.3 / 1.5.0 / 2.0.0 三个候选里挑出下一个语义化版本,并用一句话给出理由:

(a) 你修了一个 bug——之前 mean_price([]) 会返回 0.0,现在改成抛 ValueError。 (b) 你新增了一个公开函数 vwap(ticks),既有行为完全不变。 (c) 你把公开函数 mean_price 重命名为 arithmetic_mean_price,没有保留别名。

然后写出发布 (b) 这一版的 git tag -a 命令,以及一段 Keep-a-Changelog 形式的 ## [<version>] - 2026-05-24 标题加一条 ### Added 下的项目符号。

提示

按「老用户是否被迫改代码」分类:被迫改 = MAJOR;没被迫改、但能用到新东西 = MINOR;行为更贴近文档承诺 = PATCH。三个 case 各落一档。

提示

(a) bug 修正,PATCH 升 1.4.3;(b) 新增公开函数,MINOR 升 1.5.0;(c) 无别名重命名属于破坏性变更,MAJOR 升 2.0.0。tag 写 git tag -a v1.5.0;标题写 ## [1.5.0] - 2026-05-24

下一步

pyproject.toml 起步、经 pytest 测试、覆盖率与 hypothesis 加固,再到本课的构建—发版—标签,你已经能让组里另一个人用一行 pip install xyzprice 装上你写的包。下一阶段 Subject 3.2(Python for Data & Quant)把这条「能装上」的链路扩成「扛得住量化日常」的工具链——numpy/pandas/polars 的列存 I/O、pyarrow 的零拷贝交换、把回测脚本写成可发版库的标准布局。CI 自动化(pre-commit、tag push 触发的自动构建与发布)与容器化部署留给 3.6.5;此时此刻,你已经完成了 Python 工程化的第一程。