管理Python环境的教程

134 阅读15分钟

一般来说,Python是一种非常灵活的编程语言--这也延伸到了它的环境管理。不幸的是,这意味着你的Python安装很容易成为一个混乱的、错综复杂的垃圾场。有一大堆环境管理工具可以驯服这种混乱......但这最终会变得更加复杂,特别是对于一个新的 Python 开发者来说

在这篇文章中,我们将介绍现有工具的优点和缺点,这样你就可以对你的设置做出一个明智的决定。现实上,我们只需要在几个原则上达成一致。

  • 虚拟化是你的朋友:按项目隔离你的Python环境,避免项目间的依赖性冲突,使你的生活无限轻松
  • 项目应该是可重现的:你越是能严格指定依赖关系,就越容易为你自己或另一个开发者精确重现代码的运行环境。
  • 自足的 = 可部署的:打包和运送一个带有所有装饰物的环境越容易,让项目在完全不同的系统上运行就越容易(比如从开发环境移动到部署环境)。

我们的任何一个Python虚拟化解决方案都可以满足这些要求--尽管像其他任何工具一样,你会发现关于哪个是最好的意见非常强烈我们在这里的目标是让你能够对你的 Python 环境做出明智的决定,并在此过程中为自己省去一些麻烦。我们将从最简单的低级标准工具开始,然后通过较新的、更强大的 (尽管有时更深奥、更有限制) 的选项,一路讨论优点和缺点 - 我鼓励你考虑所有可能适合你的项目需要的选项。

这到底是谁的环境?

首先,较新的 Python 编码者可能想知道 - 当我们说环境控制时,我们在谈论什么?大体上,我们要认识到三个层次的控制。

  • 已安装的 python 包:我们可以在运行的 Python 实例中import 什么?
  • 安装的 python 二进制文件本身:我们实际运行的是什么版本的 Python?
  • 非 Python 的、系统级的依赖:像我们的数字包所依赖的 C/C++ 工具链是什么?

能够明确地控制一个项目的任何(或所有)这些东西,使得打包项目并在一个新的环境中启动和运行变得更加容易。在最简单的层面上,Python 中的大多数虚拟化工具会创建一个虚拟环境,看起来像下面这样。

venv/
|-- bin/
|   |-- python
|   |-- pip
|   |-- activate
|   |-- <other binaries/CLI tools>
|-- lib/
|   |-- python3.7/
|   |   |-- site-packages/
|   |   |   |-- <pip-installed packages>
|-- include/
|   |-- <underlying C header files and such>
|-- <config files>

像这样的目录包含了一个 Python 实例运行所需要的一切--二进制文件和可调用的脚本 (像一个特定环境的python 可执行文件) 在bin ,任何已安装的 Python 包在lib ,以及任何需要的额外的非 Python 头文件和配置。通过设置系统搜索路径,以这个目录为目标 (通常通过包含的激活脚本完成),我们可以运行一个完全独立的 Python 版本,其可执行文件、已安装的包等永远不会引用这个目录之外的 Python 工具。除了实现项目的独立性之外,这对于在没有root 权限的系统上运行 Python 环境是一个很好的解决方案,因为我们可以在一个有用户权限的地方创建 virtualenv。

由于虚拟环境包括它自己版本的pip 软件包管理器,向环境中安装软件包是很简单的 -- 我们只需像平常一样在环境激活的情况下pip install ,软件包就会出现在正确的地方。这也让我们可以指定环境的依赖性 - 我们可以只用一个已知的 Python 版本 (因为我们想使用相同的可执行文件) 和一个列出所需软件包及其版本的requirements.txt 文件来重新创建一个环境,这个文件可以直接传递给pip install

由于环境激活是由系统路径变量控制的,所以从技术上讲,我们可以把这个目录放在任何地方--没有必要像Node.js项目那样让环境包与项目同在一个位置。将虚拟环境与其相关的项目代码放在一起可能是有好处的 (前提是将其排除在诸如git 追踪之外,因为我们只需要实际追踪 Python 版本和需求文件),但是为所有的 Python 项目使用一个集中的目录也是同样可行的。

所以,不再多说,让我们看看我们在环境管理方面有哪些选择

内置的。venv

从3.3版本开始,Python标准库就有了一个简单的内置工具。 venv,用于创建虚拟环境。只需调用

$ python -m venv $VENV_PATH

(根据需要替换你的 venv 路径) 将在指定的目录下创建一个像上面那样的虚拟环境,同时还有一个启动脚本--这个环境可以通过调用以下命令来激活或停用

$ source $VENV_PATH/bin/activate
$ deactivate

一旦激活,pip install (单个软件包或来自需求文件)将如期工作。要打包一个虚拟环境以便在其他地方复制,你只需要生成一个包含环境内容的需求文件。

$ pip freeze > requirements.txt

与环境激活将产生一个需求文件,可以安装到另一个系统上的一个新的虚拟环境。

优点。

  • 自带Python,不需要额外的工具
  • 创建一个标准的虚拟环境,可以和几乎所有的工具一起使用:requirements.txt ,适用于任何环境管理器使用。pip

缺点。

  • 只知道已安装的软件包:用调用的任何Python来创建环境,所以你仍然坚持手动管理Python版本
  • 除了pip-可安装到环境中的东西外,没有其他的附加功能。

venv 有更多。virtualenv

实际上还有一个更早的(可以追溯到Python 2.x)工具。 virtualenv来创建这些环境。事实上,venv 是通过将virtualenv 的一个子集的功能引入 Python 3.3+ 标准库而创建的。这仍然被支持,并且可以通过pip 来安装 -- 尽管用户应该注意,这将只为当前活动的 Python 安装 (默认为系统安装)。为了避免与系统包冲突 (以及在用户缺乏安装到系统 Python 所需的权限的情况下),可以通过pip install --user virtualenv 以每个用户为单位进行安装。一旦安装,调用方法与venv 类似。

% virtualenv -p $PYTHON_CALLABLE $VENV_PATH

在指定的位置创建一个虚拟环境目录,它可以被激活/停用,就像从venv 。 值得注意的是,我们可以选择提供一个Python可调用程序--而venv 为用来调用它的Python创建一个环境,virtualenv 可以为系统上任何可用的Python安装程序创建一个环境,这意味着我们可以只从系统Python运行一个工具,为单独管理的Python安装程序创建环境(如果省略-p 选项,它将默认使用当前活动的Python版本)。一旦创建,pip install ,按名称或按需求文件工作,如预期。

优点。

  • 创建相同的标准虚拟环境,像venv 一样,可以很好地与大多数工具配合使用
  • 可以通过同样的调用为任何已安装的Python创建环境
  • 包括一些高级功能,如为环境创建引导脚本的能力

缺点。

  • 安装的Python版本仍然需要手动管理
  • 需要对系统中的Python进行软件包安装管理

扩展到安装管理:pyenvpyenv-virtualenv

上述两个解决方案都只解决了包的管理问题--在任何一种情况下,用户都要手动管理已安装的 Python 版本。幸运的是,有一个很好的 pyenv工具来解决这个问题,在 OSX 上可以通过homebrew 安装,或者直接通过git 检出和构建。

一旦设置好,新的Python版本可以通过以下方式轻松安装

$ pyenv install $PYTHON_VERSION_OR_DEFINITION_FILE

当前活动的版本或所有安装的列表可以通过以下方式显示

$ pyenv version
$ pyenv versions

用户级的默认值或项目目录下的特定 Python 版本可以通过以下方式设置

$ pyenv global $PYTHON_VERSION
$ pyenv local $PYTHON_VERSION

就其本身而言,pyenv 只控制已安装的 Python 版本,而不是虚拟环境。当然,在一个pyenv 控制的 Python 版本中,我们可以很容易地使用venvvirtualenv 来构建虚拟环境 - 但pyenv 的开发者也推出了一个插件。 pyenv-virtualenv,用于管理带有pyenv 安装的 Python 版本的环境。通过homebrewgit checkout+build 安装后,运行

$ pyenv virtualenv $PYTHON_VERSION $VENV_NAME

将使用指定的pyenv 管理的 Python 版本和给定的名称创建一个虚拟环境 (或者,可以省略 Python 版本来使用当前的默认版本)。虚拟环境可以通过以下方式激活或停用

$ pyenv activate $VENV_NAME
$ pyenv deactivate

虚拟环境的安装位置由pyenv-virtualenv 管理,所以我们不需要担心为环境指定一个目录 (在我们的项目中,或者在一个中心位置) 。然而,它是一个正常的虚拟环境,所以pip 安装、需求文件等都像我们期望的那样工作。值得注意的是,pyenv 为每个虚拟环境跟踪一个 Python 版本,所以我们实际上可以使用pyenv local 为项目目录设置一个特定的虚拟环境,而不仅仅是一个 Python 版本。

优点。

  • 不依赖Python,只依赖shell命令
  • 一站式管理所有安装的Python版本
  • 快速设置每个用户和每个项目的默认Python版本和虚拟环境,包括项目目录的自动切换
  • 轻松创建与特定Python安装相联系的虚拟环境

缺点。

  • 仅限OSX/linux(尽管存在Windows端口)
  • 有点复杂的安装/设置过程

一体化。pipenv

到目前为止,我们一直在使用多个工具来管理Python和环境,并将软件包安装到这些环境中 - 如果我们能将所有这些都整合到一个单一的工具中,会怎么样? Pipenv我们的目标就是要做到这一点,把Python和强大的软件包版本控制捆绑在一起,就像Javascript的npmyarn 那样。在通过软件包管理器 (homebrew,apt,dnf, etc.) 或pip 安装到现有的 Python 环境中 (建议作为用户级工具安装,如virtualenv),我们可以在我们的项目目录中创建一个新的pipenv 项目,用

$ pipenv --python $PYTHON_VERSION

它将使用指定的Python版本初始化项目(如果安装了pyenv ,它甚至可以按需安装Python版本)。首先,这将创建。

  • 在项目主页上创建一个Pipfile 配置文件,指定 Python 版本、来源和任何已安装的软件包
  • 一个新的虚拟环境,位于pipenv 的工作目录中。

我们不再需要分别管理pip 和虚拟环境的安装--pipenv 可以同时处理这两个问题。要安装一个包,只需运行

$ pipenv install $PACKAGE_NAME

就可以将软件包安装到虚拟环境中,并将该软件包作为依赖关系写入Pipfile中。然后这个Pipfile就是我们在其他地方重建项目所需要的,而不是其他管理器所使用的requirements.txt --只要在一个有Pipfile的目录上运行pipenv install ,就可以重新创建环境。为了激活环境。

$ pipenv shell

将使用项目的虚拟环境启动一个新的shell进程。

接下来,pipenv 可以做一些相当独特的事情--它完全确定和指定项目的依赖性。至少,pip install 只需要一个要安装的软件包名称,例如:pip install numpy 。当然,我们可以在pip install 或需求文件中指定版本限制,例如:numpy==1.18.1 。然而,除此之外,pip 并没有做太多的验证--在拉取我们想要安装的软件包的必要依赖时,pip 有可能最终拉取冲突的版本,所以除非我们真的检查了所有的安装(比如通过实际安装所有的东西,然后直接从pip freeze 生成需求文件),否则我们在试图根据软件包需求重建环境时可能会遇到问题。相反,pipenv 详尽地建立了依赖关系图,标记了任何问题,并生成了一个经过验证的Pipfile.lock ,以完全指定项目中的每个依赖关系。我们可以为我们的Pipfile中的需求手动触发这个程序,用

$ pipenv lock

来从Pipfile中提取特别要求的软件包,并为Pipfile.lock ,生成依赖关系图。虽然这确实产生了可以确定复制的环境,但依赖关系的解决可能相当复杂,所以pipenv 环境的编写速度比使用裸pip 要慢。

优点。

  • 由Python打包机构正式支持
  • 用于项目、虚拟环境和包管理的单一工具
  • pyenvconda 的 Python 和环境类型配合良好
  • 每个项目都有有效的、确定的依赖关系

缺点。

  • 与其他管理工具不兼容,因此需要在不同的项目和用户中统一使用
  • 依赖关系的解决是相当缓慢的

poetry 在运动中

类似地。 poetry包括环境控制和依赖关系解析,但它更多地是针对 Python 包的开发,而不是一般的项目控制。用自定义安装程序安装后,我们可以用下面的命令创建一个新的项目

$ poetry init

这将通过一系列的交互式提示运行,以填写一个pyproject.toml 配置文件,指定你的项目的依赖性。另一种情况是。

$ poetry new $PACKAGE_NAME

将创建一个目录结构,如

package-name/
|-- pyproject.toml
|-- README.rst
|-- package_name/
|   |-- __init__.py
|-- tests/
|   |-- __init__.py
|   |-- test_package_name.py

基本上,这已经创建了一个骨架,正是我们想要的构建Python包的结构,尽管配置的TOML文件取代了标准库的打包工具所使用的setup.py 文件。我们可以通过以下方式添加项目的依赖性

$ poetry add $PACKAGE_NAME

之后,如果需要的话,运行poetry install 将把所有指定的包安装到一个新的虚拟环境中 (如果poetry 已经在一个虚拟环境中运行,它将使用该环境),同时确保poetry.lock 文件中的依赖关系得到完全验证。对于指定项目的Python版本,poetry 可以与pyenv 集成(或者你可以通过poetry env use <path> 手动指定环境)。我们可以将这个目录用于任何代码,但它的闪光点在于Python包的构建--poetry buildpoetry publish 将组装Pythonsdistwheel 包的发行版,并将它们发布到你选择的存储库。

优点。

  • 用于项目、虚拟环境和软件包管理的单一工具
  • 每个项目都有有效的、确定的依赖关系
  • 为Python软件包提供集成的构建/发布工具

缺点。

  • 依赖关系的解决是相当缓慢的
  • 需要特定工具的安装和更新,而不是使用库存的包管理器
  • 工具更倾向于包项目,而不是像应用开发这样的事情(但仍可用于开发!)。
  • 目前只能构建纯Python轮子,所以我们不能包括C/C++依赖或Cython 集成代码等东西
  • 不能与其他环境/包管理器集成

一个挑战者出现了。anaconda

历史上,Python包管理面临着一个主要问题--虽然Python包可以要求非Python的依赖性(例如,编译的C/C++几乎是Python中所有数值工具的基础),但包不能有意义地以可控方式跟踪这些依赖性。旧的sdist ("源代码分发")软件包分发只能共享源代码,因此需要在主机上有一个兼容的编译器来构建软件包--编译器工具链之间的变化会给软件包的安装带来错误。虽然这一点通过迁移到wheel 发行版得到了极大的改善,它可以包括像共享对象.so 文件这样的编译依赖,但 Python 的软件包树并不跟踪非 Python 的依赖(因此,例如,它们并不真正了解像编译依赖中的版本变化)。

这个问题和其它问题 (比如pip 中缺乏严格的依赖关系解析器) 促使了Anaconda的开发--一个多合一的 Python 发行版 (尽管它的行为与你习惯的通常的 Python 调用程序相同),名为conda 的软件包和环境管理器,以及一种新的软件包发行格式。整个交易由一个图形化的安装程序提供,它也会在你的启动脚本中注入指令,这样默认的Python将来自Anaconda发行版。然后我们可以用conda 管理器创建新的虚拟环境,使用

$ conda create --name $ENV_NAME

激活/停用环境。

$ conda activate $ENV_NAME
$ conda deactivate

在conda环境中,只要运行

$ conda install $PACKAGE_NAME

将从 conda 仓库中提取一个软件包并将其安装到环境中。要导出一个conda 环境或从导出的文件中重新创建一个环境(相当于从requirements.txt 文件中安装)。

$ conda list --export > $REQUIREMENTS_FILE
$ conda create --name $ENV_NAME --file $REQUIREMENTS_FILE

这是 Anaconda/conda 和我们在这里讨论的其他管理器之间最大的区别--当其他一切都建立在pip 上并使用标准的wheel 格式的 Python 包时,conda 从头开始重新设计了包装在其环境中真正工作的方式,并采取了相当不同的理念。简而言之:pip任何环境中安装python依赖,而condaconda环境中安装任何依赖项。在conda 环境中,你可以精细地控制你的依赖关系,代价是只能在conda 环境框架中发挥作用 -- 软件包管理器与环境密不可分,并且依赖于与其它 Python 工具不兼容的打包结构和环境规范。相比之下,pip 在处理 Python 依赖关系的环境方面是彻底通用的 (它甚至在更详细的环境管理器 (如pipenvpoetry) 中使用),并且可以安装到基本上任何运行 Python 的环境中,包括conda 环境。

所有这些意味着Anaconda和它的相关工具可以非常强大地让你的本地环境运行起来,并对你自己的项目进行相当无痛的管理。因此,它是数据科学家的常用解决方案(直接面向数据科学家),他们通常在自己的定制环境中运行代码,并对数字包中的编译依赖性的清洁处理有特殊需求。然而,它与其他系统的集成明显要困难得多(除非它们也都在运行conda ),特别是在处理生产部署时。

优点。

  • 集成了Python分布、环境和包管理
  • 有意义地处理非Python的依赖关系
  • 包括类似于pipenv 的严格的依赖性解决方法,以及poetry
  • 提供了一个现成的数据科学栈
  • 可以跨平台工作

缺点。

  • 软件包管理没有与标准的软件包库集成,这意味着回到pip 安装可能仍然是必要的
  • 不与任何其他环境管理器集成,没有交叉兼容性
  • condapip 安装的混合搭配可能难以复制。
  • 为Python包开发提供了全新的工具链

如果它能在你的机器上运行,我们就会把你的机器运走。docker

就本文而言,这有点奇怪,但它对环境管理非常关键,值得包括在内。Docker,与这里的其他工具不同,根本就不是一个Python环境管理器--相反,它是一个 容器管理器。每个Docker容器运行一个轻量级的环境,包括所有代码、运行时、系统工具和隔离资源上的库。从开发者的角度来看,容器似乎是一个完全独立的机器,运行着Linux环境,没有完整的虚拟机的资源开销--在一台机器上并行运行一些容器是完全可行的(例如,在构建一个全栈应用时,开发者可能同时为前端、后端和数据库实例运行单独的容器)。这使我们能够完全控制我们的代码环境中的一切,直到低级别的系统依赖,并让我们创建一个可移植的容器,可以在任何运行Docker的地方完全复制该环境。

真的,学习Docker本身就值得写一篇文章,但让我们快速运行一个运行一些Python代码的例子。首先,我们的项目目录应该是这样的。

my-project/
|-- Dockerfile
|-- docker-compose.yml
|-- requirements.txt
|-- project_code/
|   |-- <your Python code goes here>

其中,我们有。

  • Dockerfile:构建Docker容器的说明。这些集合起来的说明构成了一个Docker镜像,它可以为任何数量的独立的容器实例旋转起来。
  • docker-compose.yml:运行容器的说明和设置。这是可选的,因为YAML中的所有内容都可以用docker 命令行调用来完成,但它使我们的生活变得更加容易。
  • requirements.txt 和Python代码:这就像你在任何其他环境中的Python设置。事实上,我们可以用 来代替任何其他管理器中的环境规范,例如,我们可以很容易地运行 ,并包括一个用于容器的Pipfile来代替。requirements.txt pipenv

Dockerfile 将看起来像

FROM python:3.7

WORKDIR /opt/app

COPY ./requirements.txt ./requirements.txt

RUN pip install -r requirements.txt

其中我们。

  • 声明一个 "基本镜像 "的起点:在本例中,一个运行在 Debian linux 上的官方 Python 3.7 镜像
  • 为后续指令设置当前工作目录
  • 将文件复制到容器中,这样它们就可以用于后续的命令了
  • 执行用于设置的命令:我们可以在RUN 调用任何可以在容器的操作系统的命令行中运行的命令。

这宣告了一个安装了所需Python版本的容器,然后引入并安装我们所需的包的依赖性--我们也可以使用RUN 调用来安装任何必要的系统依赖性(C编译器、git 调用来引入源代码,等等)。为了构建和运行容器,我们可以使用docker 命令行调用,但通常使用docker-compose包装器工具。我们以YAML格式指定这方面的设置。

version: '3.7'

services:
  python-app:
    build:
      context: ./
    volumes:
      - ./python_code:/opt/app/python_code
    command: python

这让我们可以为容器指定各种有用的信息:速记的构建指令(所以docker-compose 知道如何从Dockerfile 构建一个镜像),在容器和主机之间共享代码和数据的卷挂载(所以我们可以现场编辑我们的代码并重新运行,而不需要重建容器),用于系统控制的端口映射和环境变量,以及我们真正希望它运行的命令。要构建我们的容器,我们只需要运行

$ docker-compose build

这将为compose文件中指定的任何服务组装一个镜像。然后运行

$ docker-compose run python-app

将在我们的命令行中启动一个Python shell,在容器中运行(或者,对于后台服务,我们可以使用docker-compose up )。

最后,我们应该花点时间考虑使用哪个基础镜像,因为有很多选择。从技术上讲,我们可以选择一个Linux发行版的基础映像,然后在上面安装Python(例如,使用pyenv )--但是如果我们对操作系统没有特别的需求,我们可以很容易地使用库存的python 映像。这就给了我们一个 Debian 环境,标签中列出的 Python 版本是它的系统 Python(例如,python:3.7.5 将默认使用 Python 3.7.5),并且没有冲突,所以我们可以继续运行,像平常一样。另外,我们可以使用 "slim "图像(例如,python:3.7.5-slim ),它剥离了一些潜在的不必要的系统依赖。我们可能需要把这些东西再加进去--例如,某些有编译依赖的软件包需要gcc ,用于C语言编译器--但与 "全脂 "版本相比,它通常会产生一个小得多的镜像,并且可以很容易地用Dockerfile 指令重新安装依赖。

来自其他语言的开发者可能会熟悉alpine 镜像类型--这是一个为安全和尺寸限制而设计的极其精简的发行版。Alpine python镜像确实存在,但我强烈建议不要使用它们。Alpine的所有C语言依赖都使用musl 工具链(同样是为了尺寸和安全),而Python软件包一般都是以glibc 工具链为基础建立其依赖的,这是为了性能的原因。让这些包在Alpine上运行需要重建glibc 工具链,或者为musl 重建这些包--无论哪种方式,对开发者来说都是一个头疼的问题,并且可能导致镜像的大小与slim 版本相当,从而消除了首先使用Alpine的优势。除非你对Alpine有特殊的部署需求,否则我建议你默认使用纤细的Python。另外,如果你已经熟悉并喜欢使用Anaconda工具,有支持良好的anacondaminiconda (一个剥离的、较小的发行版)Docker基础镜像。

优点。

  • 完全控制我们所有的依赖关系,直到系统级别 - 甚至可以使用本文中的任何环境管理器
  • 明确指定所有指令来复制代码环境
  • 容器可以很容易地被打包和运送到生产环境上运行
  • 共同的指令可以被构建到新的基础镜像中,并在项目之间重复使用
  • 可以以编程方式定义在单个docker-compose 规格中独立运行的多个服务之间的互动关系

缺点。

  • 需要学习全新的API
  • alpine 镜像上的构建工具有一些奇怪的变数

总结

当然,我说过这些工具中的任何一个对于环境管理都是可行的--这是真的,但这并不意味着我没有自己的观点在所有这些中,我更喜欢使用pyenv/pyenv-virtualenv 在我的本地机器上进行控制,尽管对于大多数项目,我会继续使用Docker(如果我不想以root身份运行,可能会使用venv ),以便对我的代码有最大程度的控制和最简单的部署。几乎在所有情况下,我都会默认使用纤细的Python镜像,因为这在镜像大小和消除不必要的依赖之间取得了良好的平衡,同时确保我可以轻松地获得我需要的工具。

我通常发现依赖关系的解决不是一个问题(特别是对于高度孤立的项目),因此发现像pipenvpoetry 这样的工具不是很有必要--尽管这对于大型项目来说是个好主意,而且更好的依赖关系解决是Python 软件基金会的一个高级优先事项。在项目有重大依赖性问题的情况下,在Docker容器上运行pipenv 是一个很好的解决方案。

处理生产环境的困难,再加上较新的wheel 格式的Python包(自Anaconda开发后引入)提供的处理编译依赖的好处,意味着我宁愿完全避免将conda 作为环境管理器。虽然它的市场定位是数据科学家,但我发现,一个看起来更像生产的代码设置,再加上Docker提供的一致的构建环境,从长远来看是值得的。