Python 项目最恶心的翻车,经常不在代码里,而是在 A 电脑能跑,换到 B 电脑连依赖都装不上。
一开始只是 pip install -r requirements.txt 报错。继续往下看,发现同事用 Python 3.12,本地是 3.10;某个包在 macOS 有 wheel,到了 Linux 镜像里开始编译;CI 里缺系统库,GDAL 找不到头文件;最后大家开始在群里互发“我这边可以”的截图。
这种时候再讨论 venv、Conda、Poetry、Rye,就不是工具偏好了。说白了,谁能少添乱,谁就赢。
我的习惯比较土:小脚本用 venv,碰到 CUDA、GDAL 这类二进制依赖就上 Conda/Mamba,老项目已经 Poetry 跑顺了就别折腾,新开的普通应用现在更愿意用 uv。Rye 就不往新项目里放了,它那条线已经被 uv 接过去。
venv 适合小活,团队流程另说
venv 的好处很硬:标准库自带。
不用额外安装,不用等新工具维护,也没有什么学习成本。它基于已有 Python 创建一个隔离目录,包都装到这个目录里,删了重建也不心疼。
python -m venv .venv
source .venv/bin/activate
python -m pip install requests
python -m pip freeze > requirements.txt
小脚本、一次性爬虫、内部巡检工具、课程示例,用 venv 没什么问题。打开目录一看 .venv、requirements.txt,不用想太多。
麻烦从多人协作开始。
venv 只负责隔离环境,不负责回答这些问题:Python 版本谁统一,依赖谁来锁,开发依赖和运行依赖怎么分,命令入口写哪儿,CI 怎么保证和本地一致。
最后通常变成 README 里几行命令撑场面:
python -m venv .venv
pip install -r requirements.txt
pytest
这套在简单项目里没问题。一旦依赖多了,requirements.txt 很容易变成“当时某台机器上 pip freeze 出来的遗照”。里面既有项目需要的包,也有间接依赖,还可能混进本地调试装过的东西。
比如项目只直接用了 fastapi 和 uvicorn,pip freeze 出来可能是一长串:
anyio==...
click==...
fastapi==...
h11==...
pydantic==...
starlette==...
uvicorn==...
这些包当然都参与运行,但它们不全是人主动选的依赖。后面想升级 FastAPI,谁是直接依赖、谁是被带进来的,就开始含糊。
更常见的翻车是 Python 版本。一个人用 3.11,一个人用 3.12,CI 镜像里还是 3.10。某个依赖在新版本 Python 上没有 wheel,开始本地编译,报错一路钻到 gcc 和系统库。大家嘴上都说“就是装依赖”,其实已经不是同一个环境了。
所以 venv 可以用,前提是边界很小。项目会长期维护、有 CI、有开发依赖、有多个同事参与,再加一层更完整的工具会省不少后账。
Conda 管的是二进制麻烦
Conda 经常被 Web 后端嫌弃,慢、重、环境目录大,和 pip 混着用还容易把环境弄脏。
这些吐槽多数成立,只是 Conda 本来就不是给普通 Web 项目准备的。
Conda 要处理的很多东西压根不只是 Python 包。它会把 C/C++ 动态库、系统级依赖、不同平台的二进制包一起纳入环境。pip 装一个包,背后可能只是下载 wheel;Conda 装一套环境,背后经常是在处理一组原生库之间的兼容关系。
比如 GIS:
conda create -n geo python=3.11 geopandas rasterio gdal
conda activate geo
用 venv + pip 也能硬装,运气不好就开始编译 GDAL、找头文件、配动态库路径。装到最后,技术含量没增长多少,耐心倒是掉得很快。
深度学习也类似。PyTorch、CUDA、cuDNN、NumPy、SciPy、OpenCV 这些东西搅在一起,环境稍微歪一点,报错就往底层库跑。Conda 或 Mamba 就是少受点这种罪。
科研、算法、数据分析、Notebook、模型训练环境,这些场景用 Conda 很正常。团队靠 environment.yml 复现环境,也比每个人手搓一遍底层库靠谱。
一个 environment.yml 至少能把环境边界写清楚:
name:train
channels:
-conda-forge
dependencies:
-python=3.11
-numpy
-pandas
-pytorch
-pip
-pip:
-transformers
这东西不一定优雅,但模型训练、Notebook、地理数据处理这些活,能复现就已经谢天谢地了。
普通 Web 服务上来就 Conda,通常有点重。
FastAPI、Django、Flask、Celery、SQLAlchemy、Pydantic 这类纯 Python 或已有 wheel 的依赖,用 Conda 经常显得笨重。环境大一点、求解慢一点、镜像构建多绕一点,日常开发都能感受到。
Conda 和 pip 可以混用,不过顺序要收着点。比较稳的做法是先用 Conda 装 Python 和底层库,再用 pip 补 PyPI 上没有的包;反过来一通乱装,后面 Conda 求解环境时不一定能把 pip 那部分也处理得很漂亮,复现会变难。
Poetry 适合已经进了工程化阶段的项目
Poetry 当年能火起来,主要靠它把 Python 项目里散落的几件事收拢了。
依赖声明进 pyproject.toml,解析结果进 poetry.lock,开发依赖可以分组,构建和发布也有统一入口。
poetry init
poetry add fastapi uvicorn
poetry add --group dev pytest ruff
poetry install
poetry run pytest
这对团队协作很有用。新人拉代码不用猜装哪几个文件,CI 不用散落一堆 pip install -r xxx.txt,库项目发到 PyPI 也顺手。
Poetry 比 pip freeze 好用的一点,是它分得清“我主动要什么”和“解析器最后装了什么”。
很多老项目的 pyproject.toml 会这么写直接依赖:
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.115.0"
uvicorn = "^0.34.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
ruff = "^0.11.0"
poetry.lock 里才是完整解析结果。代码评审时看到有人加了 fastapi,比看到 requirements.txt 突然多出十几个包要清楚得多。
新一点的 Poetry 项目也可以按 PEP 621 把运行依赖写到 [project] 里,这块不用纠结。关键还是那件事:直接依赖和锁定结果别搅在同一个文件里。
它的问题是有点重。
Poetry 有自己的虚拟环境策略、依赖解析习惯、配置方式。老项目里已经有 setup.py、requirements.txt、内部私有源、Docker 构建脚本,再硬塞 Poetry,迁移成本会冒出来。
还有应用项目和库项目的差别。
库项目需要考虑打包元数据、版本号、构建、发布、可选依赖,Poetry 这套很顺。应用项目只关心“把这个服务按锁定依赖装出来并跑起来”,发布到 PyPI 那些能力可能用不上。
所以 Poetry 我一般不劝人立刻换掉。团队已经跑顺了,poetry.lock 进仓库,CI 也稳定,继续用没毛病。只是新开一个普通服务时,我不会再下意识把 Poetry 当默认答案。
Rye 已经让位给 uv
Rye 的路线很讨人喜欢。
Python 版本、虚拟环境、依赖、项目、工具安装,一套命令全包。对被 pyenv + venv + pip + pip-tools + pipx + poetry 搞烦的人来说,这个方向确实舒服。
但现在讨论 Rye,重点已经转到“还要不要新用”。
官方文档写得很直:Rye 不再开发,建议用户迁到 uv;安装页也提醒新用户去装 uv。后面没有安全更新,没有新功能计划,也不会继续追着 Python 工具链变化跑。
个人电脑上已有 Rye 项目,能跑就先跑,找个空档迁就行。Rye 和 uv 的理念接近,很多项目迁起来不用推倒重来,更多是把 [tool.rye] 那些配置换成 uv 的写法。
新项目继续选 Rye,账不好算。刚教会团队 rye sync,后面又要解释为什么迁 uv sync,这不像技术品味,更像给自己排明年的活。
uv 少拼一堆工具
uv 没在题目选项里,但现在聊 Rye 绕不开它。
它能创建虚拟环境、安装依赖、管理 Python 版本、跑脚本、装命令行工具、生成 lock 文件,也提供 uv pip 这种兼容旧工作流的入口。以前要拼好几个工具的路,现在很多都能从 uv 走。
新项目大概这样:
uv init demo
cd demo
uv add requests
uv add --dev pytest ruff
uv run pytest
已有 requirements.txt 的项目,也不用一口气重构:
uv venv
uv pip install -r requirements.txt
这个过渡挺实在。很多团队有能力迁工具,但怕迁工具顺手把项目结构、依赖声明、Docker、CI 全动了。uv 可以先从“更快地装依赖、更稳定地建环境”开始用,后面再决定要不要整理成完整的 pyproject.toml + uv.lock。
uv 快,这个大家都知道。更省心的是它顺手把 Python 版本也管了。比如项目需要 3.12,机器上没有,uv 可以按需下载托管的 Python;项目里也能用 .python-version 表达版本意图。以前这块常靠 pyenv、系统 Python、CI 镜像各管各的,时间长了很容易漂。
普通服务里大概这样:
uv python pin 3.12
uv add fastapi uvicorn
uv add --dev pytest ruff
uv sync
仓库里留下 pyproject.toml、uv.lock、.python-version,新机器进来直接 uv sync,比“先装 pyenv,再装 Python,再建 venv,再 pip install”短很多。
lock 也值得单独说。
纯 requirements.txt 更像安装清单,能不能跨平台、能不能解释清楚直接依赖和间接依赖,要看生成方式。Poetry 有 poetry.lock,uv 有 uv.lock,都是为了让项目在不同机器上尽量装出同一套结果。应用项目尤其吃这一点,线上跑的东西最好少靠“pip 今天解析出来什么就是什么”。
uv 也不用捧得太高。
公司已经有 Poetry 模板、私有源、发布脚本、审计流程,换 uv 要改 CI、Docker、缓存策略、开发文档,这些都是真成本。科研团队 Conda 环境跑得好好的,为了统一工具硬换也没多大意思;工具换得越底层,翻车时越难解释。
大概就这么选
我一般看项目会在哪儿翻车。
只有一两个文件,依赖也就 requests、beautifulsoup4 这种,venv 就够。建个 .venv,留个 requirements.txt,小活不用套大流程。
项目会长期维护,有测试、有 lint、有开发依赖、有 CI,要么 Poetry,要么 uv。已有 Poetry 继续跑,新项目可以优先试 uv,尤其是服务类应用和内部工具。
项目要发包给别人用,Poetry 仍然稳。uv 也能构建和发布,但 Poetry 在这块积累更久,团队熟的话,没多少理由为了新鲜感换。
环境里出现 CUDA、PyTorch、GDAL、Rasterio、SciPy 这类原生依赖密集的东西,Conda/Mamba 可以直接排上。在 pip 编译报错里证明自己很能扛,最后只是把时间烧给动态库。
Rye 就停在老项目维护和迁移通道里。新项目继续押它,眼前收益不明显,后续迁移倒是挺确定。
Python 这套工具链一直乱,也正常。有人写十行脚本,有人发 PyPI 包,有人训模型,有人跑线上服务,非要一个工具全包,最后多半又绕回骂 Python。
反正我现在新项目不会选 Rye。小活 venv,重二进制 Conda/Mamba,老 Poetry 项目先稳住,新应用优先 uv。
最后有个挺现实的判断。
一个项目如果半年没人碰,再打开还能顺手跑起来,环境工具就没白选。要是每次重启老项目都像考古,先找 Python 版本,再猜依赖文件,再翻群聊问当年谁装过 GDAL,那换什么工具都只是晚点受罪。
Python 写久了会发现,环境管理这事不高级,也不性感,但它特别会挑时间找麻烦。