版本锁定概念

版本锁定,说白了就是把项目里用到的每个依赖包都固定成一个具体的版本号。你想想看,一个 Python 项目少说几十个包,多的几百个。每个包还在不断更新。如果今天能跑,明天同事 pull 下来就崩了,多半是版本没锁住。

我个人习惯,拿到一个新项目第一件事就是检查 environment.yml 或者 requirements.txt 里有没有锁定版本。没有的话,先锁上再跑代码。这习惯救过我不少次。

为什么需要锁定版本

  • 重现性 – 同一个项目,换台机器、换个时间,必须能跑出同样的结果。版本不锁定,就等于把命运交给上游的发布节奏。
  • 避免依赖冲突 – A 包要 numpy<1.25,B 包要 numpy>=1.26。不锁版本,conda 可能帮你装了个两头都不满足的版本。我在项目中遇到过好几次这种“隐形炸弹”。
  • 安全性与合规 – 审计时要知道每个包的精确版本,才能查 CVE 漏洞列表。锁了版本,一查就知道。
  • 协作一致性 – 团队多人开发,用同一个锁文件,所有人环境下依赖完全一致,再也不会出现“我本地能跑你不行”的扯皮情况。

核心原则:开发环境、测试环境、生产环境的依赖树必须完全一致。版本锁定是实现这个原则最直接的手段。

版本号规范

常见的版本号长这样:2.5.1、1.0.0rc1、0.3.0+build.202405。本质上就是 主版本号.次版本号.修订号[-预发布标签][+构建元数据] 这种格式。

  • 主版本号 – 不兼容的 API 变更。从 1.x 升到 2.x,老代码很可能要改。
  • 次版本号 – 新增功能,但向后兼容。从 1.2.x 升到 1.3.x,一般没问题。
  • 修订号 – bug 修复,只改内部实现不改接口。放心升。
  • 预发布标签 – 如 alpha、beta、rc,表示还没正式发布,稳定性较低。

嗯,这里要注意:版本比较不是简单的字符串比较。1.0.0rc1 比 1.0.0 小,因为预发布标签的优先级低于正式发布。我曾经在 CI 里吃过这个亏,写了个自动升级脚本,结果把 1.0.0rc2 当成了比 1.0.0 新的版本……

语义化版本简介

语义化版本(Semantic Versioning,简称 SemVer)是一套约定,让版本号本身能传递兼容性信息。规范很简单:MAJOR.MINOR.PATCH,对应上面说的主、次、修订。

实际运用技巧:你在 environment.yml 里锁定版本时,建议用 = 精确锁到 Patch 级别,比如 numpy=1.24.3。不要只锁主版本,那样等于没锁。

举个例子:

# 不锁版本(危险)
dependencies:
  - numpy

# 锁到主版本(半锁定)
dependencies:
  - numpy>=1.24,<2.0

# 精确锁定(推荐)
dependencies:
  - numpy=1.24.3
  - pandas=2.0.2
  - scikit-learn=1.3.0

学会看版本号,就能快速判断一个升级的风险等级:

版本变化风险等级说明
1.2.3 → 1.2.4低Patch 修复,放心升级
1.2.3 → 1.3.0中Minor 新增功能,可能引入新依赖
1.2.3 → 2.0.0高Major 大版本,必须测试所有 API 调用
1.2.3 → 1.2.3b1高预发布版本,不推荐生产使用

注意:Python 生态中很多包并没有严格遵守 SemVer。比如 NumPy 2.0 之前,一直使用 1.x 版本号,但内部 API 破坏性变更也不少。所以不要盲目相信版本号,关键依赖还是要做回归测试。

语义化版本核心流程 MAJOR 1.x → 2.x MINOR 1.2.x → 1.3.x PATCH 1.2.3 → 1.2.4 预发布标签:-alpha, -beta, -rc 优先级低于正式版,例如 1.0.0-rc1 < 1.0.0 构建元数据:+build.202405 仅用于构建标识,不影响版本优先级比较 版本比较规则:MAJOR > MINOR > PATCH > 预发布 数字比较,预发布标签只要存在就比正式版小

我自己在维护一个开源工具的时候,每次发布都会严格按照 SemVer 来定版本号。用户看到版本号就知道升级会不会出问题,这一点对社区信任很有帮助。你也应该养成这个习惯。不管是写包的还是用包的,把版本锁清楚,能省下大把排查依赖冲突的时间。