2. 虚拟环境与依赖锁定

虚拟环境与依赖锁定 学习卡片

为什么“能装”不等于“可复现”

直接执行 pip install requests 只会把当前最新版装进环境,依赖树里每个包的版本由解析器当场决定。换一台机器、换一天再装,解析结果可能不同。依赖锁定的核心是把“解析结果”固化成文件:声明文件写人类维护的顶层依赖与约束,锁定文件写解析后的完整版本与哈希,安装时只按锁定文件执行。

依赖声明 → 解析 → 锁定 → 可复现安装 声明文件(人工维护) pyproject.toml requests>=2.31 flask>=3.0,<4.0 只写顶层依赖与范围 解析器 pip-compile / poetry lock 锁定文件(生成) requirements.txt requests==2.31.0 flask==3.0.2 含全部传递依赖 + 哈希 安装 pip install -r 机器 A requests 2.31.0 机器 B requests 2.31.0 CI 容器 requests 2.31.0 同一锁定文件 → 版本一致

用 pip-tools 完成锁定与复现

pip-tools 的工作流是“声明 → 编译 → 同步”。声明文件只写顶层依赖,编译生成带精确版本和哈希的锁定文件,同步则让环境与锁定文件严格一致。

先安装工具并创建隔离环境:

python -m venv .venv
# Linux / macOS
source .venv/bin/activate
# Windows
# .venv\Scripts\activate

python -m pip install --upgrade pip
pip install pip-tools

在项目根目录创建 requirements.in,只写顶层依赖:

# requirements.in
requests>=2.31
flask>=3.0,<4.0

编译生成锁定文件。加 --generate-hashes 让每个包附带哈希,安装时可校验下载内容:

pip-compile requirements.in --generate-hashes -o requirements.txt

生成的 requirements.txt 会包含全部传递依赖,形如:

flask==3.0.2 \
    --hash=sha256:... \
    --hash=sha256:...
requests==2.31.0 \
    --hash=sha256:...
# 以及 werkzeug、jinja2、click 等传递依赖

在目标机器上按锁定文件安装,并开启哈希校验:

pip install -r requirements.txt --require-hashes
锁定文件必须提交到版本库,声明文件同样提交。升级依赖时改 requirements.in 后重新 pip-compile,不要手改锁定文件。

用 Poetry 完成锁定与复现

Poetry 把声明与锁定统一在 pyproject.toml 和 poetry.lock 中,锁定文件由 poetry lock 生成,安装时默认读取锁定文件。

# pyproject.toml 片段
[tool.poetry.dependencies]
python = "^3.11"
requests = ">=2.31"
flask = ">=3.0,<4.0"
poetry lock          # 解析并写入 poetry.lock
poetry install       # 按 poetry.lock 安装,保证版本一致
poetry install --sync  # 让环境与锁定文件严格对齐

Poetry 的锁定文件记录每个包的精确版本、来源与哈希,poetry install 在已有锁定文件时不会重新解析,因此不同机器结果一致。

验证可复现性

复现验证的关键是“同一锁定文件、不同环境、版本一致”。下面用脚本对比两个环境中的实际版本:

# verify_lock.py
import json
import subprocess
import sys

def freeze() -> dict:
    out = subprocess.check_output(
        [sys.executable, "-m", "pip", "freeze"], text=True
    )
    result = {}
    for line in out.splitlines():
        if "==" in line:
            name, ver = line.split("==", 1)
            result[name.strip().lower()] = ver.strip()
    return result

if __name__ == "__main__":
    env_a = freeze()
    print(json.dumps(env_a, indent=2, sort_keys=True))

在机器 A 与机器 B 分别运行该脚本,比较输出。若锁定文件一致且安装命令正确,requests、flask 及其传递依赖的版本应完全相同。也可用一条命令直接比对:

# 机器 A 导出
pip freeze > freeze_a.txt
# 机器 B 导出后比对
pip freeze > freeze_b.txt
diff freeze_a.txt freeze_b.txt   # 无输出即一致
若比对出现差异,优先检查:是否误用 pip install requests 绕过锁定文件;是否在锁定文件之外手动装了包;是否用了不同 Python 版本导致部分包解析出不同版本。

锁定文件检查清单

检查项通过标准
声明文件只含顶层依赖无传递依赖被手写进去
锁定文件已提交版本库中存在 requirements.txt 或 poetry.lock
安装使用锁定文件pip install -r 或 poetry install,未直接装包
哈希校验开启pip 使用 --require-hashes
跨机版本一致diff 两个环境的 freeze 输出无差异