文章

Conda开发两则

和Python相处是一个非常血压高的话题,这次要分享的是Conda的两则内容...

Conda开发两则

conda是一个集成了包管理和环境管理功能的工具,支持在不同操作系统(如Windows、macOS和Linux)上创建独立的Python环境,并安装包括Python包和系统级依赖(如C库)的软件包。conda拥有自己的包格式和仓库(如Anaconda和conda-forge),可以自动处理复杂的依赖关系,适用于需要稳定、可重复的软件环境的科研、数据分析、机器学习等场景。

常见的Python环境管理器还有venv和pip。venv是Python标准库自带的工具,用于创建隔离的虚拟环境,但不包括包管理功能;pip是Python的官方包管理工具,只能安装Python包,对非Python依赖和环境隔离没有支持。Conda相较于它们更全面,既能管理Python环境,也能管理各种语言和系统依赖,且支持在同一系统中并行存在多个独立环境,适合需要严格控制依赖版本的开发任务。

此外,mamba也可以进行包和环境的管理,它是Conda的高速替代品。mamba使用C++重写了解析器和下载逻辑,相较于Conda具有更快的依赖解析速度和更少的内存占用,特别适用于大型项目和复杂环境的构建。和Conda的区别是,mamba保留了Conda的命令接口和兼容性,但执行效率明显提高,在需要频繁创建和更新环境时更加高效。

考虑到mamba的知名度和边缘包的兼容程度不如Conda,且很多库的包管理实质上也是使用的Conda,因此下文的叙述还是以使用Conda的经验介绍为主。

Conda or Pip?

最近在复现和调试的一个项目是Sat-NeRF,它有一个包有问题:gdal

GDAL(Geospatial Data Abstraction Library)是一个用于读写和转换地理空间栅格和矢量数据格式的库,支持上百种GIS数据格式,如GeoTIFF、Shapefile、KML、GeoJSON等。它是地理信息系统(GIS)、遥感、地图处理等领域的基础库,常用于空间数据的读取、投影转换、图像处理等任务。

而经过上面的对比,不难发现Pip无法管理非Python生态的库,如gdal的依赖libtiff和libpoppler,这也就意味着gdal只能使用Conda进行版本管理。在图形学这部分,由于经常使用CMake构建其系统级库,所以有着大量的类似的库,如pyproj、rasterio、pycolmap、open3d等,它们也最好只用Conda进行版本管理。

Conda和Pip的包

事实上,顶会中很多论文里面开源的代码,由于经过的时间太长,而库的版本不明确,导致所涉及的依赖都已经存在版本冲突,Pip/Conda事实上并不善于处理这种冲突,混用二者容易产生问题

需要使用Conda来安装的库,比如gdal,通常都有着极为严格的依赖关系,比如这里的gdal依赖于libpoppler和libgdal,然而在Sat-NeRF——一个将近3年没有更新的库中,它的requirements写的是有问题的:

1
2
3
4
5
6
7
### create satnerf venv
conda create -n satnerf -c conda-forge python=3.6 libgdal
conda activate satnerf
pip install --ignore-installed certifi -r requirements.txt
pip install torch==1.7.1+cu110 torchvision==0.8.2+cu110 torchaudio===0.7.2 -f https://download.pytorch.org/whl/torch_stable.html
conda deactivate
echo "satnerf conda env created !"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# requirements.txt
gdal
rpcm
opencv-contrib-python
jupyter
Pillow
chardet
matplotlib
numpy
affine==2.3.0
fire==0.4.0
kornia==0.5.3
plyflatten==0.2.0
pyproj==3.0.1
pytorch_lightning==1.3.7
torchmetrics==0.6.0
PyYAML<=5.4.1,>=5.1
rasterio==1.2.10
scipy==1.4.1
srtm4==1.2.1
utm==0.7.0
scikit_image==0.16.2
numba==0.53.1

这就引出第二个问题,在Conda和Pip的协同环境下,包是如何被管理的?

Cover 在全局和局部的作用域下…

如图所示,在同一个Conda虚拟环境中,Conda和Pip安装的包共用一套路径,因此并不隔离。这种设计会导致两者之间的依赖产生冲突。

在Conda的issue #9449中指出,在早期的poppler库中存在一个变动:

Indeed, poppler changed its soname at 0.62, and our libgdal package was built with 0.60.1, with a constraint for only the next major version. We’ll rebuild our libgdal package.

结合前面的安装步骤,实际发生如下情况:

Conda在创建环境时安装了 libgdalpython=3.6,配置是正常的。随后,pip install -r requirements.txt 尝试重新安装 gdal,但因为没有指定版本,Pip试图构建或拉取的 gdal 与已有的 libgdalpoppler 不兼容,导致安装挂起或失败。Pip此时可能已部分覆盖了环境中的依赖包。

若这时用Conda试图重新安装或降级 poppler,由于Pip已更改部分依赖的版本,且 gdal 的可用版本与 python=3.6 约束下的依赖范围非常狭窄,Conda在解析依赖图时难以求解,最终卡死或失败。由错误信息 ImportError: libpoppler.so.91: cannot open shared object file 可见,系统在寻找一个不存在的版本,说明当前环境中依赖已彻底错乱。

根本原因是:gdal依赖的系统库必须由Conda统一管理,Pip无法正确处理此类系统级依赖。

最终的解决方案是在Sat-NeRF的后继版本中找到:不再预装 libgdal,而是将 gdal 交由Conda完整安装,让Conda自动拉取兼容的 libgdalpoppler,从而避免了依赖冲突。

本文由作者按照 CC BY 4.0 进行授权