让 conda 负责 C/Fortran 扩展的编译产物,让 uv 负责项目级依赖隔离——各取所长,而不是互相替代。
问题的根源:科学计算栈的「编译地狱」
Python 科学计算的基石——NumPy、SciPy、scikit-learn、PyTorch——本质上不是 Python 包。它们是 C、C++、Fortran 编写的原生扩展,Python 只是薄薄一层胶水壳。你在 import numpy 时,真正加载的是 _multiarray_umath.cp312-win_amd64.pyd(Windows)、_multiarray_umath.cpython-312-x86_64-linux-gnu.so(Linux)这类编译后的二进制共享库。
这就带来一个根本性矛盾:
| 工具 | 设计哲学 | 对 C 扩展的态度 |
|---|---|---|
| pip / uv | 从源码构建或下载预编译 wheel | 依赖系统工具链(GCC/GFortran/LAPACK/BLAS),失败率极高 |
| conda | 分发预编译二进制 | 所有依赖(包括 BLAS/OpenMP)都打包进环境,开箱即用 |
uv 再快,也快不过编译 NumPy 的 Fortran 源码——它做的事情和 pip 一样,只是更快地下载、更快地解析依赖图。面对 scipy 背后那个庞大的 Fortran 代码库(MINPACK、ODEPACK、FFTPACK、ARPACK……),你需要的不是一个更快的包管理器,而是一个根本不用编译的方案。这正是 conda 存在的意义。
架构设计:conda 做底座,uv 做上层
核心思路一句话:把 C 扩展密集的公共依赖下沉到 conda 环境作为底座,然后用 --system-site-packages 让每个项目的 uv 虚拟环境继承这个底座。
direction: down
scientific-base: {
label: "conda 底座环境 \"scientific-base\""
near: top-center
content: |md
Python 3.12
numpy · scipy · scikit-learn · numba
pandas · matplotlib · seaborn
── 全部预编译 C/Fortran 扩展 ──
BLAS/OpenMP/编译器运行时 一并打包
|
}
inheritance: {
label: "--system-site-packages"
shape: text
style.font-style: italic
}
scientific-base -> inheritance -> project-a
scientific-base -> inheritance -> project-b
scientific-base -> inheritance -> project-c
project-a: {
label: "项目 A (.venv, uv)"
polars
plotly
opencv
}
project-b: {
label: "项目 B (.venv, uv)"
xgboost
lightgbm
prophet
}
project-c: {
label: "项目 C (.venv, uv)"
altair
streamlit
h3-py
}
layers: {
platform: {
label: "conda 底座"
}
computation: {
label: "uv 项目层"
}
}
底层原理:Python 的模块查找链
这里涉及一个被大多数人忽略的 Python 知识:虚拟环境不是"隔离"的,它是"层叠"的。
当你执行 python -m venv --system-site-packages .venv(或 uv venv --system-site-packages)时,uv 会在 .venv/pyvenv.cfg 中写入一行:
include-system-site-packages = true
Python 启动时读取这个标记,决定 sys.path 的构造方式。默认情况下(false),虚拟环境的 site-packages 完全替代系统路径;设为 true 后,系统路径追加到虚拟环境路径之后:
# --system-site-packages 后的 sys.path 结构:
[
'.venv/Lib/site-packages', # [0] uv 安装的包,优先级最高
'C:/Users/xxx/miniforge3/envs/scientific-base/Lib/site-packages', # [1] conda 底座
'C:/Users/xxx/miniforge3/envs/scientific-base/Lib', # [2] 标准库
...
]
这意味着一件事:conda 底座里装的包,uv 项目里不装也能直接 import。 只有当你需要在某个项目中覆盖某个包的版本时,才用 uv pip install 把它装到 .venv 里,新版本会因路径顺序更高而「遮蔽」conda 的版本。
这是整个架构的地基。不是 hack,是 Python 虚拟机原生的标准机制。
实战流程:从零搭建三层结构
第一层:conda 科学计算底座
# 只做一次,所有项目共享
conda create -n scientific-base \
python=3.12 \
numpy scipy "scikit-learn>=1.5" \
pandas matplotlib \
"libblas=*=*mkl" \
-c conda-forge
几个关键点:
- 显式指定 BLAS 后端:
libblas=*=*mkl确保用 Intel MKL 而不是默认的 OpenBLAS。NumPy 和 SciPy 的线性代数性能(np.dot、np.linalg、SVD、特征值分解)严重依赖 BLAS 实现。MKL 在 Intel CPU 上比 OpenBLAS 快 20%~50%,而且 conda 把 MKL 运行时一并打包,不需要系统层面安装任何东西。 - conda-forge 优先:default channel 的包通常更旧,conda-forge 维护者更活跃且 ABI 兼容性测试更充分。
- 底座保持干净:只放「多个项目都需要且涉及 C 扩展」的包。纯 Python 包(比如 tqdm、rich、pyyaml)不要放底座——让 uv 按需安装。
第二层:uv 项目虚拟环境
# 首先激活 conda 底座
conda activate scientific-base
# 进入项目目录
cd D:\projects\biomech-modeling
# 创建继承底座的 uv 虚拟环境
uv venv --system-site-packages --python 3.12
# 激活 uv 虚拟环境
.venv\Scripts\activate
此时你的 shell 里,python 指向 .venv/Scripts/python.exe,sys.path 包含 conda 底座路径。验证:
import sys
for p in sys.path:
print(p)
# .venv/Lib/site-packages
# .../miniforge3/envs/scientific-base/Lib/site-packages
第三层:项目专用依赖
# 只装这个项目需要的轻量包
uv pip install polars plotly pyarrow pydantic
# 如果需要覆盖 conda 底座的某个包版本
uv pip install "scipy==1.14.0" # 装到 .venv,遮蔽 conda 的版本
到这里,biomech-modeling 项目拥有:
- conda 底座的全部科学计算能力(numpy, scikit-learn, matplotlib…)
- 自己独立的 polars、plotly 等纯 Python 依赖
- 完全不影响其他项目
依赖锁定:让底座可复现
conda 底座本身也需要锁定。不要靠记忆重建环境,而是维护一个 environment.lock.yml:
# scientific-base/environment.lock.yml
name: scientific-base
channels:
- conda-forge
dependencies:
- python=3.12.7=h2628f8c_0_cpython
- numpy=2.1.3=py312hbb9b9f5_0
- scipy=1.14.1=py312hbb9b9f5_0
- scikit-learn=1.5.2=py312hbb9b9f5_0
- pandas=2.2.3=py312hbb9b9f5_0
- matplotlib=3.9.2=py312hbb9b9f5_0
- libblas=3.9.0=25_win64_mkl
- libcblas=3.9.0=25_win64_mkl
- liblapack=3.9.0=25_win64_mkl
# ... 完整依赖树
生成锁文件:
conda list --explicit > environment.explicit.txt # 精确到 build string
conda env export --from-history > environment.lock.yml # 人类可读
这样,换机器或重装时一键还原:
conda env create -f environment.lock.yml
版本遮蔽的风险与纪律
前面提到,你可以用 uv pip install numpy==2.0 覆盖 conda 底座的版本。但这可能炸。
原因在于 C ABI 兼容性。conda 底座里的 scikit-learn 是针对底座里的 numpy 版本编译的,它们的 C 结构体布局、函数签名、内存对齐都是一致的。如果你在 .venv 里覆盖了一个 ABI 不兼容的 numpy 版本:
# conda 底座: numpy 1.26.4 (C API version 0x00000010)
# uv .venv: numpy 2.1.0 (C API version 0x00000012)
# scikit-learn 在底座里,链接了 numpy 1.26.4 的 C API
# → segfault 或静默数据损坏
铁律:你想覆盖底座里的某个包 → 要么新建一个 conda 底座,要么把所有依赖它的包也一并用 uv 覆盖(这样做就失去了 conda 底座的意义)。
所以我推荐的规则是:
| 包类型 | 谁负责 | 理由 |
|---|---|---|
| C/Fortran 扩展包 | conda 底座 | 避免编译,保证 ABI 一致 |
| 大型 ML 框架(PyTorch) | conda 底座 | CUDA/cuDNN 依赖链极复杂 |
| 纯 Python 包 | uv 项目层 | 解析快、版本灵活 |
| 轻量 C 扩展但版本敏感 | uv 项目层 + --system-site-packages |
遮蔽 conda 版本 |
进阶:针对不同项目类型的底座策略
传统数值计算(NumPy/SciPy 为主)
一个通用底座就够了。所有项目共享同一个 scientific-base。
深度学习项目
PyTorch 和 TensorFlow 的 CUDA 依赖极其复杂,建议每个 CUDA 版本独立一个底座:
conda create -n torch-cu121-base python=3.12 pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
conda create -n torch-cu124-base python=3.12 pytorch torchvision torchaudio pytorch-cuda=12.4 -c pytorch -c nvidia
uv 项目再选择继承对应的底座。
地理空间计算
GDAL/GEOS/PROJ 等底层 C 库有严格的版本耦合:
conda create -n geo-base python=3.12 \
geopandas rasterio shapely fiona pyproj \
gdal libgdal \
-c conda-forge
这些库如果交给 pip/uv 从源码编译,依赖的系统库差异会导致难以排查的运行时错误——坐标转换偏移、投影信息丢失。conda 的二进制分发是唯一可靠方案。
这套架构的本质
这是 Python 虚拟环境设计之初就预留的扩展点。--system-site-packages 的本意是让虚拟环境继承系统 Python 自带的包(比如 Linux 发行版通过 apt 安装的 python3-numpy),我们只是把「系统 Python」换成了「conda 预编译的科学计算环境」,把「虚拟环境工具」换成了更快、更现代的 uv。
apt → conda(都是系统级/环境级二进制包管理器)
venv → uv venv(都是虚拟环境管理器,后者更快)
总结
Conda 解决的是编译问题(二进制分发、ABI 兼容性、BLAS/CUDA 运行时打包),uv 解决的是速度问题(依赖解析、下载、虚拟环境创建)。把它们对立起来是没意义的——它们在 Python 包管理的技术栈里分别占据不同的层次:
| 层次 | 工具 | 关注点 |
|---|---|---|
| 运行时与系统库 | conda | C/Fortran ABI、BLAS 后端、CUDA、编译器运行时 |
| 项目级依赖隔离 | uv | 快速解析、虚拟环境、版本锁定 |
二者不是「选哪个」,而是「怎么组合」。