Python 依赖管理:你应该选择哪个工具?

Poetry、Pip 和 Conda 的深度比较

Image by Author

本文最初发布于https://mathdatasimplified.com,发布日期为2023年6月13日。

动机

随着数据科学项目的扩大,依赖关系的数量也增加了。为了保持项目环境的可重现性和可维护性,使用有效的依赖管理工具非常重要。

因此,我决定比较三种流行的依赖管理工具:Pip、Conda 和 Poetry。经过仔细评估,我相信 Poetry 在效率和性能方面优于其他两个选项。

在本文中,我们将深入探讨 Poetry 的优点,并突出其与 Pip 和 Conda 的主要区别。

可用的软件包

拥有广泛的软件包选择,使开发人员能够更轻松地找到最适合其需求的特定软件包和版本。

Conda

有些软件包,如“snscrape”,无法使用 conda 安装。此外,某些版本,如 Pandas 2.0,可能无法通过 Conda 安装。

虽然可以在 conda 虚拟环境中使用 pip 来解决软件包限制,但 conda 无法跟踪使用 pip 安装的依赖关系,从而使依赖关系管理变得具有挑战性。

$ conda list# packages in environment at /Users/khuyentran/miniconda3/envs/test-conda:## Name                    Version                   Build  Channel$ conda list # packages in environment at /Users/khuyentran/miniconda3/envs/test-conda: # # Name Version Build Channel

Pip

Pip 可以从 Python Package Index (PyPI) 和其他存储库安装任何软件包。

Poetry

Poetry 也允许从 Python Package Index (PyPI) 和其他存储库安装软件包。

依赖关系数量

减少环境中的依赖关系可以简化开发过程。

Conda

Conda 提供完整的环境隔离,管理 Python 软件包和系统级依赖关系。这可能导致与其他软件包管理器相比更大的软件包大小,可能在安装和分发过程中消耗更多的存储空间。

$ conda install pandas$ conda list          # packages in environment at /Users/khuyentran/miniconda3/envs/test-conda:## Name              Version         Build           Channel             blas                1.0             openblas                          bottleneck          1.3.5           py311ha0d4635_0                    bzip2               1.0.8           h620ffc9_4                        ca-certificates     2023.05.30      hca03da5_0                        libcxx              14.0.6          h848a8c0_0                        libffi              3.4.4           hca03da5_0                        libgfortran         5.0.0           11_3_0_hca03da5_28                 libgfortran5        11.3.0          h009349e_28                       libopenblas         0.3.21          h269037a_0                        llvm-openmp         14.0.6          hc6e5704_0                        ncurses             6.4             h313beb8_0                        numexpr             2.8.4           py311h6dc990b_1                    numpy               1.24.3          py311hb57d4eb_0                    numpy-base          1.24.3          py311h1d85a46_0                    openssl             3.0.8           h1a28f6b_0                        pandas              1.5.3           py311h6956b77_0                    pip                 23.0.1          py311hca03da5_0                    python              3.11.3          hb885b13_1                        python-dateutil     2.8.2           pyhd3eb1b0_0                      pytz                2022.7          py311hca03da5_0                    readline            8.2             h1a28f6b_0                        setuptools          67.8.0          py311hca03da5_0                    six                 1.16.0          pyhd3eb1b0_1                      sqlite              3.41.2          h80987f9_0                        tk                  8.6.12          hb8d0fd4_0                        tzdata              2023c           h04d1e81_0                        wheel               0.38.4          py311hca03da5_0                    xz                  5.4.2           h80987f9_0                        zlib                1.2.13          h5a0b063_0      

Pip

Pip只安装软件包所需的依赖项。

$ pip install pandas$ pip listPackage         Version--------------- -------numpy           1.24.3pandas          2.0.2pip             22.3.1python-dateutil 2.8.2pytz            2023.3setuptools      65.5.0six             1.16.0tzdata          2023.3

Poetry

Poetry也只安装软件包所需的依赖项。

$ poetry add pandas$ poetry shownumpy           1.24.3 在Python中进行数组计算的基本软件包pandas          2.0.2 用于数据分析、时间序列等领域的强大数据结构python-dateutil 2.8.2 Python标准datetime模块的扩展pytz            2023.3 现代和历史时区定义的提供者six             1.16.0 Python 2和3兼容性实用程序tzdata          2023.3 IANA时区数据的提供者

卸载软件包

卸载软件包及其依赖项可以释放磁盘空间、防止不必要的混乱并优化存储资源的使用。

Pip

Pip仅删除指定的软件包,而不删除其依赖项,可能会导致未使用的依赖项随着时间的推移而积累。这可能会导致存储空间使用增加和潜在冲突。

$ pip install pandas$ pip uninstall pandas$ pip listPackage         Version--------------- -------numpy           1.24.3pip             22.0.4python-dateutil 2.8.2pytz            2023.3setuptools      56.0.0six             1.16.0tzdata          2023.3

Conda

Conda会删除软件包及其依赖项。

$ conda install -c conda pandas$ conda uninstall -c conda pandas收集软件包元数据(repodata.json):完成解决环境:完成## Package Plan ##  environment location: /Users/khuyentran/miniconda3/envs/test-conda  removed specs:    - pandas以下软件包将被删除:  blas-1.0-openblas  bottleneck-1.3.5-py311ha0d4635_0  libcxx-14.0.6-h848a8c0_0  libgfortran-5.0.0-11_3_0_hca03da5_28  libgfortran5-11.3.0-h009349e_28  libopenblas-0.3.21-h269037a_0  llvm-openmp-14.0.6-hc6e5704_0  numexpr-2.8.4-py311h6dc990b_1  numpy-1.24.3-py311hb57d4eb_0  numpy-base-1.24.3-py311h1d85a46_0  pandas-1.5.3-py311h6956b77_0  python-dateutil-2.8.2-pyhd3eb1b0_0  pytz-2022.7-py311hca03da5_0  six-1.16.0-pyhd3eb1b0_1Proceed ([y]/n)? 准备事务:完成验证事务:完成执行事务:完成Poetry

Poetry

Poetry也会删除软件包及其依赖项。

$ poetry add pandas$ poetry remove pandas  • 正在删除numpy (1.24.3)  • 正在删除pandas (2.0.2)  • 正在删除python-dateutil (2.8.2)  • 正在删除pytz (2023.3)  • 正在删除six (1.16.0)  • 正在删除tzdata (2023.3)

依赖关系文件

依赖关系文件通过指定所需软件包的确切版本或版本范围来确保软件项目环境的可重现性。

这有助于在不同系统或不同时间点重新创建相同的环境,确保具有相同依赖项的开发人员之间的协作。

Conda

为了在Conda环境中保存依赖项,您需要手动将它们写入文件。在环境.yml文件中指定的版本范围可能会导致安装不同的版本,在重新生成环境时可能会引入兼容性问题。

假设我们以安装pandas 1.5.3版本为例。这是一个指定依赖项的环境.yml文件的示例:

# 环境.yml文件名: test-condachannels:  - defaultsdependencies:  - python=3.8  - pandas>=1.5

如果新用户尝试在pandas的最新版本为2.0时重现环境,则会安装pandas 2.0版本。

# 创建并激活虚拟环境$ conda env create -n env$ conda activate env# 列出当前环境中的软件包$ conda list...pandas                   2.0

如果代码库依赖于特定于pandas 1.5.3版本的语法或行为,并且在版本2.0中更改了语法,则使用pandas 2.0运行代码可能会引入错误。

Pip

pip也可能出现同样的问题。

# requirements.txt pandas>=1.5

# 创建并激活虚拟环境$ python3 -m venv venv$ source venv/bin/activate# 安装依赖项$ pip install -r requirements.txt# 列出软件包$ pip listPackage    Version---------- -------pandas       2.0...

您可以通过将它们固定在requirements.txt文件中来锁定版本:

$ pip freeze > requirements.txt

# requirements.txtnumpy==1.24.3pandas==1.5.3python-dateutil==2.8.2pytz==2023.3six==1.16.0

但是,这会使代码环境变得不够灵活,并且在长期维护过程中可能更难维护。对依赖项进行任何更改都需要手动修改requirements.txt文件,这可能耗时且容易出错。

Poetry

Poetry在安装软件包时会自动更新pyproject.toml文件。

在下面的示例中,使用版本约束^1.5添加“pandas”软件包。这种灵活的版本控制方法确保您的项目可以适应新版本而无需手动调整。

$ poetry add 'pandas=^1.5'

# pyproject.toml[tool.poetry.dependencies]python = "^3.8"pandas = "^1.5"

poetry.lock文件存储每个软件包及其依赖项的精确版本号。

# poetry.lock...[[package]]name = "pandas"version = "1.5.3"description = "Powerful data structures for data analysis, time series, and statistics"category = "main"optional = falsepython-versions = ">=3.8"[package.dependencies]numpy = [    {version = ">=1.20.3", markers = "python_version < \"3.10\""},    {version = ">=1.21.0", markers = "python_version >= \"3.10\""},    {version = ">=1.23.2", markers = "python_version >= \"3.11\""},]python-dateutil = ">=2.8.2"pytz = ">=2020.1"tzdata = ">=2022.1"...

这确保了安装的软件包的一致性,即使在pyproject.toml文件中指定了版本范围。在这里,我们可以看到安装了pandas 1.5.3而不是pandas 2.0。

$ poetry install$ poetry show pandasname         : pandas                                                                  version      : 1.5.3                                                                   description  : Powerful data structures for data analysis, time series, and statistics dependencies - numpy >=1.20.3 - numpy >=1.21.0 - numpy >=1.23.2 - python-dateutil >=2.8.1 - pytz >=2020.1

为开发和生产分别指定依赖项

通过分离依赖项,您可以清楚地区分用于开发目的(如测试框架和代码质量工具)的软件包和用于生产环境的软件包(通常包括核心依赖项)。

Conda

Conda 不支持为不同环境分别设置依赖项,但是一种解决方法是创建两个环境文件:一个用于开发环境,一个用于生产环境。开发文件包含生产和开发依赖项。

# environment.ymlname: test-condachannels:  - defaultsdependencies:  # Production packages  - numpy  - pandas

# environment-dev.ymlname: test-conda-devchannels:  - defaultsdependencies:  # Production packages  - numpy  - pandas  # Development packages  - pytest  - pre-commit

Pip

Pip 也不直接支持分别设置依赖项,但是可以使用类似的方法来使用不同的需求文件。

# requirements.txtnumpy pandas

# requirements-dev.txt-r requirements.txtpytestpre-commit

# Install prod$ pip install -r requirements.txt# Install both dev and prod$ pip install -r requirements-dev.txt

Poetry

Poetry 通过在一个文件中支持组来简化管理依赖关系。这使您可以在一个地方跟踪所有的依赖项。

$ poetry add numpy pandas$ poetry add --group dev pytest pre-commit

# pyproject.toml[tool.poetry.dependencies]python = "^3.8"pandas = "^2.0"numpy = "^1.24.3"[tool.poetry.group.dev.dependencies]pytest = "^7.3.2"pre-commit = "^3.3.2"

只安装生产依赖项:

$ poetry install --only main

安装开发和生产依赖项:

$ poetry install

更新环境

更新依赖项对于从较新的包版本中获得错误修复、性能改进和新功能至关重要。

Conda

Conda 允许您仅更新指定的包。

$ conda install -c conda pandas$ conda install -c anaconda scikit-learn

# 新版本可用$ conda update pandas$ conda update scikit-learn

之后,您需要手动更新 environment.yaml 文件,以使其与更新的依赖项保持同步。

$ conda env export > environment.yml

Pip

Pip 也仅允许您更新指定的包,并需要您手动更新 requirements.txt 文件。

$ pip install -U pandas

$ pip freeze > requirements.txt

Poetry

使用 Poetry,您可以使用 update 命令升级 pyproject.toml 文件中指定的所有包。此操作会自动更新 poetry.lock 文件,确保包规范和锁定文件之间的一致性。

$ poetry add pandas scikit-learn# 新版本可用poetry updateUpdating dependenciesResolving dependencies... (0.3s)Writing lock filePackage operations: 0 installs, 2 updates, 0 removals  • Updating pandas (2.0.0 -> 2.0.2)  • Updating scikit-learn (1.2.0 -> 1.2.2)

依赖关系解决

依赖冲突会在所需的项目中需要的包或库具有冲突版本或不兼容依赖项时发生。正确解决冲突对于避免错误、运行时问题或项目失败至关重要。

Pip

pip 逐个安装包,这意味着它按照指定的顺序一个一个地安装每个包。这种顺序方法有时会导致冲突,当包具有不兼容的依赖项或版本要求时。

例如,假设您首先安装 pandas==2.0.2,它需要 numpy>=1.20.3。稍后,您使用 pip 安装 numpy==1.20.2。即使这会创建依赖冲突,pip 也会继续更新 numpy 的版本。

$ pip install pandas==2.0.2$ pip install numpy==1.22.2Collecting numpy=1.20.2  Attempting uninstall: numpy    Found existing installation: numpy 1.24.3    Uninstalling numpy-1.24.3:      Successfully uninstalled numpy-1.24.3ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.pandas 2.0.2 requires numpy>=1.20.3; python_version < "3.10", but you have numpy 1.20.2 which is incompatible.Successfully installed numpy-1.20.2

Conda

Conda使用SAT求解器来探索所有包版本和依赖项的组合,以找到兼容的集合。

例如,如果现有包对其依赖项有特定约束(例如,statsmodels == 0.13.2需要numpy> = 1.21.2,<2.0a0),而您要安装的包不符合该要求(例如,numpy <1.21.2),则conda不会立即引发错误。相反,它会勤勉地搜索所有所需软件包及其依赖关系的兼容版本,仅在找不到合适的解决方案时报告错误。

$ conda install 'statsmodels==0.13.2'
$ conda search 'statsmodels==0.13.2' --infodependencies:
   - numpy> = 1.21.2, = 21.3
   - pandas> = 1.0
   - patsy> = 0.5.2
   - python> = 3.9, = 1.3
$ conda install 'numpy <1.21.2'...
Package ca-certificates conflicts for:
python=3.8 -> openssl [version = '>= 1.1.1t,<1.1.2a'] -> ca-certificates
openssl -> ca-certificates
ca-certificates
cryptography -> openssl [version = '> 1.1.0,<3.1.0'] -> ca-certificates
Package idna conflicts for:
requests -> urllib3 [version = '>= 1.21.1,<1.27'] -> idna [version = '>= 2.0.0']
requests -> idna [version = '>= 2.5,<3 | >= 2.5,<4']
idna
pooch -> requests -> idna [version = '>= 2.5,<3 | >= 2.5,<4']
urllib3 -> idna [version = '>= 2.0.0']
Package numexpr conflicts for:
statsmodels == 0.13.2 -> pandas [version = '>= 1.0'] -> numexpr [version = '>= 2.7.0 | >= 2.7.1 | >= 2.7.3']
numexpr
pandas == 1.5.3 -> numexpr [version = '>= 2.7.3']
Package patsy conflicts for:
statsmodels == 0.13.2 -> patsy [version = '>= 0.5.2']
patsy
Package chardet conflicts for:
requests -> chardet [version = '>= 3.0.2,<4 | >= 3.0.2,<5']
pooch -> requests -> chardet [version = '>= 3.0.2,<4 | >= 3.0.2,<5']
Package python-dateutil conflicts for:
statsmodels == 0.13.2 -> pandas [version = '>= 1.0'] -> python-dateutil [version = '>= 2.7.3 | >= 2.8.1']
python-dateutil
pandas == 1.5.3 -> python-dateutil [version = '>= 2.8.1']
Package setuptools conflicts for:
numexpr -> setuptools
pip -> setuptools
wheel -> setuptools
setuptools
python = 3.8 -> pip -> setuptools
pandas == 1.5.3 -> numexpr [version = '>= 2.7.3'] -> setuptools
Package brotlipy conflicts for:
urllib3 -> brotlipy [version = '>= 0.6.0']
brotlipy
requests -> urllib3 [version = '>= 1.21.1,<1.27'] -> brotlipy [version = '>= 0.6.0']
Package pytz conflicts for:
pytz
pandas == 1.5.3 -> pytz [version = '>= 2020.1']
statsmodels == 0.13.2 -> pandas [version = '>= 1.0'] -> pytz [version = '>= 2017.3 | >= 2020.1']

虽然这种方法增加了找到解决方案的机会,但在处理大型环境时可能会计算密集。

诗歌

通过专注于项目的直接依赖项,Poetry的确定性解析器缩小了搜索空间,使解析过程更加高效。它评估指定的约束条件,例如版本范围或特定版本,并立即识别任何冲突。

$ poetry add 'seaborn==0.12.2'$ poetry add 'matplotlib<3.1' 因为 poetry shell 依赖于 seaborn(0.12.2),而 seaborn 又依赖于 matplotlib(>=3.1,<3.6.1 || >3.6.1),所以需要 matplotlib。因此,由于 poetry shell 依赖于 matplotlib(<3.1),版本解决失败。

这种即时反馈有助于防止潜在问题升级,并允许开发人员在开发过程的早期解决问题。例如,在下面的代码中,我们可以放宽对 seaborn 的要求,以便安装 matplotlib 的特定版本:

poetry add 'seaborn<=0.12.2'  'matplotlib<3.1' Package operations: 1 install, 2 updates, 4 removals  • Removing contourpy (1.0.7)  • Removing fonttools (4.40.0)  • Removing packaging (23.1)  • Removing pillow (9.5.0)  • Updating matplotlib (3.7.1 -> 3.0.3)  • Installing scipy (1.9.3)  • Updating seaborn (0.12.2 -> 0.11.2)

结论

总之,Poetry相对于pip和conda提供了几个优点:

  1. 广泛的包选择: Poetry提供了访问PyPI上可用的各种包的访问权限,允许您利用项目的多样化生态系统。
  2. 高效的依赖管理: Poetry仅为指定的包安装必要的依赖项,减少了环境中不必要的包的数量。
  3. 简化的包删除: Poetry简化了包及其相关依赖项的删除,使得维护清洁和高效的项目环境变得容易。
  4. 依赖解析: Poetry的确定性解析器有效地解析依赖项,及时识别和解决任何不一致或冲突。

虽然Poetry可能需要您的团队成员花费一些额外的时间和精力来学习和适应,但使用类似Poetry的工具可以在长期内为您节省时间和精力。

我喜欢写关于数据科学概念和玩不同的数据科学工具的文章。您可以通过以下方式了解我的最新帖子:

  • 订阅我的Data Science Simplified电子报。
  • 与我在LinkedIn和Twitter上联系。