RSS
菜单
全部文章快讯开发科技深度热点

包管理与虚拟环境(Python 从精通到入门 · 11)

内容摘要

把「在我电脑上能跑」变成「在哪都能跑」:虚拟环境隔离依赖,pyproject.toml 声明真相,锁文件保证重现。

把「在我电脑上能跑」变成「在哪都能跑」:虚拟环境隔离依赖,pyproject.toml 声明真相,锁文件保证重现。

你将学到

  • 为什么必须用虚拟环境,以及 venv 的正确用法
  • pip + requirements.txt 的传统工作流及其缺陷
  • pyproject.toml 与现代打包规范(PEP 517/621)
  • poetry 与 uv 的定位对比和常用命令
  • 依赖版本约束语义(~=、>=、==)与锁文件
  • 发布到 PyPI 的流程,以及 src layout 项目结构最佳实践

前置知识

先读 上一篇:类型注解与 mypy,本文的示例会用到类型注解。

一、为什么需要虚拟环境

Python 的包默认装到「全局环境」。一旦多个项目依赖同一个包的不同版本,就冲突了:

# 全局装 Django 4.x,另一个老项目需要 3.x —— 二者不可能共存
pip install django==4.2

venv 给每个项目一个独立的 site-packages,互不干扰。

# 在当前目录创建名为 .venv 的虚拟环境
python -m venv .venv

# 激活(Windows Git Bash / PowerShell / Linux 各有不同)
source .venv/Scripts/activate      # Windows Git Bash
.venv\Scripts\Activate.ps1         # Windows PowerShell
source .venv/bin/activate          # macOS / Linux

# 激活后,python 和 pip 都指向这个环境
which python                       # 输出: .../项目/.venv/Scripts/python
python -m pip install requests

# 退出
deactivate

永远用 python -m pip 而不是裸 pip——前者保证用的是当前解释器的 pip,能避免装错环境的经典事故。

python -m pip install --upgrade pip   # 顺手升级 pip 自身

二、pip + requirements.txt

最朴素的工作流:导出当前依赖清单,别人照着装。

pip freeze > requirements.txt     # 导出(包含所有间接依赖)
pip install -r requirements.txt   # 安装

requirements.txt 长这样:

requests==2.31.0
flask>=3.0,<4.0

它的缺陷很明确:

  • 不分直接依赖和间接依赖:pip freeze 会把整棵依赖树平铺出来,你无法判断哪些是自己想要的。
  • 没有锁文件语义:不同时间解析出的间接依赖版本可能不同,重现性差。
  • 环境与打包混为一谈:它描述"这个环境装了啥",不描述"这个包本身依赖啥"。

requirements.txt 依然适合部署场景(生成一份冻结清单),但不适合描述项目元数据。

三、pyproject.toml 与现代打包

PEP 518/517 引入了 pyproject.toml 作为构建配置的统一入口,PEP 621 规定了项目元数据的标准字段。

[build-system]
requires = ["hatchling"]              # 构建后端
build-backend = "hatchling.build"

[project]
name = "my-tool"
version = "0.1.0"
description = "一个示例工具"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
authors = [{ name = "Ada", email = "ada@example.com" }]

# 直接依赖(运行所需)
dependencies = [
    "requests>=2.31",
    "click~=8.1",
]

# 可选依赖,按场景分组
[project.optional-dependencies]
dev = ["pytest>=8.0", "mypy", "ruff"]
docs = ["sphinx"]

# 命令行入口:my-tool 命令对应 my_tool.cli:main
[project.scripts]
my-tool = "my_tool.cli:main"

装上构建后端后就能构建和发布:

python -m pip install build
python -m build            # 生成 dist/*.whl 和 dist/*.tar.gz
# 输出: Successfully built my_tool-0.1.0.tar.gz and my_tool-0.1.0-py3-none-any.whl

pyproject.toml 现在是唯一正确的项目配置中心:依赖、构建、工具链(mypy/ruff/pytest)都往里塞。

四、poetry 与 uv

poetry:老牌一体化方案

pipx install poetry          # 推荐用 pipx 独立安装
poetry new my-project        # 脚手架
poetry init                  # 已有项目里互动式生成配置
poetry add requests          # 添加依赖,自动写 pyproject 并更新锁文件
poetry add --group dev pytest
poetry install               # 按 poetry.lock 精确安装
poetry run pytest            # 在项目环境里执行命令
poetry lock                  # 只更新锁文件
poetry build                 # 构建发行包

poetry.lock 记录每个包的确切版本和哈希,保证「同样的锁文件装出同样的环境」。

uv:Rust 写的新一代工具

uv 用 Rust 实现,安装解析速度比 pip 快一到两个数量级,且兼容 pip 工作流。

# 安装(独立二进制)
curl -LsSf  | sh

uv venv                       # 创建 .venv
uv pip install requests       # 兼容 pip 语义,但极快
uv pip install -r requirements.txt

# 自带的项目管理(对标 poetry)
uv init
uv add requests               # 写入 pyproject.toml + uv.lock
uv add --dev pytest
uv sync                       # 按 uv.lock 同步环境
uv run pytest
uv lock

怎么选

维度 poetry uv
速度 一般 极快
生态成熟度 高,历史项目多 新,但发展迅猛
pip 兼容 自己的解析器 可直接当 pip 用
适用 团队已有 poetry 习惯 新项目、追求速度

建议:新项目直接上 uv(顺便还能当 pip 用);老项目沿用 poetry 即可,别为了赶时髦迁移。

五、版本约束与锁文件

版本约束写法(PEP 440):

==1.2.3      # 精确锁定
>=1.2,<2.0   # 区间
~=1.4.2      # 兼容发布:等价于 >=1.4.2, <1.5.0
~=1.4        # 等价于 >=1.4, <2.0
!=1.5.0      # 排除某版本

~= 最容易被误解,记住它锁定的是倒数第二段:~=1.4.2 允许到 1.4.x,~=1.4 允许到 1.x。

区分两个文件:

  • pyproject.toml 的 dependencies:写范围,表达能力与意图(人读的)。
  • 锁文件(poetry.lock / uv.lock):写确切版本,保证可重现(机器用的)。

两者都要提交到版本控制——前者决定「允许什么」,后者决定「这次用什么」。

六、发布到 PyPI

pip install build twine
python -m build                     # 生成 dist/
twine check dist/*                  # 检查元数据合法性

# 先传到测试仓库验证
twine upload --repository testpypi dist/*

# 没问题再传正式仓库(需要 PyPI API Token)
twine upload dist/*

用 Token 而不是账号密码,配在 ~/.pypirc 或环境变量里:

export TWINE_USERNAME=__token__
export TWINE_PASSWORD=pypi-AgEIcHlwaS5vcmc...

注意 name 在 PyPI 上全局唯一,先想好名字。版本号一旦上传不可覆盖,只能发新版本。

七、项目结构:src layout

推荐的目录结构:

my-project/
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── src/
│   └── my_tool/              # 包名 = 导入名
│       ├── __init__.py
│       ├── cli.py
│       └── core.py
└── tests/
    ├── test_core.py
    └── conftest.py

为什么用 src/ 而不是把包放根目录:

  • 强制「必须先安装才能导入」,避免测试时误用当前目录的源码而掩盖打包问题。
  • 测试与源码物理分离,导入路径干净。

包名用小写下划线(my_tool),项目名用连字符(my-project),PyPI 上二者等价但项目名更友好。

常见坑

坑 1:忘了激活环境,装到全局去了

# ❌ 忘记激活,requests 装进了系统 Python
pip install requests
# ✅ 先激活,再用 python -m pip 双重保险
source .venv/bin/activate
python -m pip install requests   # 确定装进 .venv

坑 2:把 .venv 提交进 git

# .gitignore
.venv/
__pycache__/
*.egg-info/
dist/
build/

.venv 是机器相关的二进制目录,绝不能进版本控制。锁文件反而必须提交。

坑 3:requirements.txt 与 pyproject 依赖打架

# ❌ 两处各写一份依赖,时间一长必然不一致
# pyproject.toml 里写 requests>=2.31
# requirements.txt 里又写 requests==2.28.0
# ✅ pyproject 作为唯一真相源,requirements 由它导出
# uv:  uv export --format requirements-txt > requirements.txt
# poetry: poetry export -f requirements.txt --output requirements.txt

小结

  • 每个项目一个虚拟环境,用 python -m venv .venv 创建,python -m pip 安装。
  • requirements.txt 适合导出部署清单,不适合描述项目元数据。
  • pyproject.toml 是现代项目配置中心(PEP 621),依赖、构建、工具链都在这里。
  • poetry 成熟、uv 快;新项目推荐 uv,它还能直接当 pip 用。
  • 依赖范围写进 pyproject.toml,确切版本写进锁文件,两者都提交。
  • 发布用 build + twine,版本号不可覆盖。
  • 用 src/ layout,测试与源码分离,强迫走「安装后导入」。

延伸阅读

  • PEP 517 / 518 / 621 / 440 —— 构建系统与版本规范
  • uv 官方文档
  • poetry 官方文档

上一篇:类型注解与 mypy · 下一篇:测试 pytest 与 TDD

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《包管理与虚拟环境(Python 从精通到入门 · 11)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。