在经历了大量用 Python 开发项目的艰辛之后,我们希望与他人分享我们的项目。这些人可以是你的朋友或同事。也许他们对你的代码不感兴趣,但他们想运行它并从中获得实际用途。例如,你创建了一个回归模型,可以根据输入特征预测某个值。你的朋友想提供他们自己的特征,看看你的模型会预测什么值。但是,随着你的 Python 项目越来越大,将其发送给朋友而不只是一个小脚本就没那么简单了。可能有许多支持文件、多个脚本,还有对库列表的依赖。要正确处理所有这些可能是一个挑战。
完成本教程后,你将学到:
- 如何通过将代码模块化来增强代码以方便部署
- 如何为模块创建包,以便我们可以依赖
pip
来管理依赖项 - 如何使用 venv 模块创建可重现的运行环境
启动你的项目,阅读我的新书 Python for Machine Learning,其中包括分步教程和所有示例的Python 源代码文件。
让我们开始吧!
Python 项目部署入门课程
照片作者:Kelly L。部分权利保留。
概述
本教程分为四个部分;它们是
- 从开发到部署
- 创建模块
- 从模块到包
- 为项目使用 venv
从开发到部署
当我们完成一个 Python 项目时,有时我们不想把它束之高阁,而是想让它成为一项例行工作。我们可能已经完成了机器学习模型的训练,并积极使用训练好的模型进行预测。我们可能构建了一个时间序列模型,并使用它进行下一步预测。然而,每天都会有新数据进来,所以我们需要重新训练它以适应发展,并保持未来的预测准确。
无论是什么原因,我们都需要确保程序能按预期运行。然而,这可能比我们想象的要困难。一个简单的 Python 脚本可能不是一个困难的问题,但随着我们的程序因为更多的依赖而变得越来越大,许多事情都可能出错。例如,我们使用的库的新版本可能会破坏工作流程。或者我们的 Python 脚本可能会运行一些外部程序,而这些程序在操作系统升级后可能会停止工作。另一种情况是,程序依赖于位于特定路径的某些文件,但我们可能会不小心删除或重命名一个文件。
总会有让我们的程序执行失败的方法。但我们有一些技术可以使它更加健壮和可靠。
创建模块
在之前的帖子中,我们展示了如何使用以下命令检查代码片段的完成时间
1 |
python -m timeit -s 'import numpy as np' 'np.random.random()' |
同时,我们也可以将其用作脚本的一部分并执行以下操作
1 2 3 4 5 |
import timeit import numpy as np time = timeit.timeit("np.random.random()", globals=globals()) print(time) |
Python 中的 import
语句允许您通过将其视为模块来重用在另一个文件中定义的函数。您可能想知道如何使模块不仅提供函数,还能成为可执行程序。这是帮助部署代码的第一步。如果我们能使我们的模块可执行,用户就不需要理解我们的代码结构就可以使用它。
如果我们的程序足够大,包含多个文件,那么将其打包成模块会更好。Python 中的模块通常是一个带有清晰入口点的 Python 脚本文件夹。因此,发送给其他人更方便,也更容易理解流程。此外,我们可以为模块添加版本,并让 pip
跟踪已安装的版本。
一个简单、单文件的程序可以写成如下形式
1 2 3 4 5 6 7 8 |
import random def main(): n = random.random() print(n) if __name__ == "__main__": main() |
如果我们将其保存为本地目录中的 randomsample.py
,我们可以通过以下方式运行它:
1 |
python randomsample.py |
或者
1 |
python -m randomsample |
并且我们可以通过以下方式在另一个脚本中重用函数:
1 2 3 |
import randomsample randomsample.main() |
这是有效的,因为魔法变量 __name__
仅在脚本作为主程序运行时才为 "__main__"
,而在从另一个脚本导入时则不是。有了这个,您的机器学习项目可能可以打包如下:
1 2 3 4 5 6 |
regressor/ __init__.py data.json model.pickle predict.py train.py |
现在,regressor
是一个包含这五个文件的目录。而 __init__.py
是一个**空文件**,只是为了表明该目录是一个可以 import
的 Python 模块。train.py
脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import os import json import pickle 来自 sklearn.linear_model 导入 LinearRegression def load_data(): current_dir = os.path.dirname(os.path.realpath(__file__)) filepath = os.path.join(current_dir, "data.json") data = json.load(open(filepath)) return data def train(): reg = LinearRegression() data = load_data() reg.fit(data["data"], data["target"]) return reg |
predict.py
的脚本是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import os import pickle import sys import numpy as np def predict(features): current_dir = os.path.dirname(os.path.realpath(__file__)) filepath = os.path.join(current_dir, "model.pickle") with open(filepath, "rb") as fp: reg = pickle.load(fp) return reg.predict(features) if __name__ == "__main__": arr = np.asarray(sys.argv[1:]).astype(float).reshape(1,-1) y = predict(arr) print(y[0]) |
然后,我们可以在 regressor/
的父目录下运行以下命令来加载数据并训练线性回归模型。然后我们可以用 pickle 保存模型:
1 2 3 4 5 6 |
import pickle from regressor.train import train model = train() with open("model.pickle", "wb") as fp: pickle.save(model, fp) |
如果我们把这个 pickle 文件移到 regressor/
目录中,我们也可以在命令行中这样做来运行模型:
1 |
python -m regressor.predict 0.186 0 8.3 0 0.62 6.2 58 1.96 6 400 18.1 410 11.5 |
这里,数字参数是模型的输入特征向量。如果我们进一步移出 if
块,即创建一个名为 regressor/__main__.py
的文件,内容如下:
1 2 3 4 5 6 7 8 |
import sys import numpy as np from .predict import predict if __name__ == "__main__": arr = np.asarray(sys.argv[1:]).astype(float).reshape(1,-1) y = predict(arr) print(y[0]) |
然后我们就可以直接从模块中运行模型了:
1 |
python -m regressor 0.186 0 8.3 0 0.62 6.2 58 1.96 6 400 18.1 410 11.5 |
请注意上面示例中 from .predict import predict
这行代码使用了 Python 的相对导入语法。这应该在模块内部使用,用于从同一个模块的其他脚本导入组件。
想开始学习机器学习 Python 吗?
立即参加我为期7天的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
从模块到包
如果您想将 Python 项目作为最终产品分发,那么能够通过 pip install
命令将您的项目安装为一个包会很方便。这很容易做到。既然您已经为项目创建了一个模块,您需要补充一些简单的设置说明。现在您需要创建一个项目目录,并将您的模块放在里面, ساتھ一个 pyproject.toml
文件,一个 setup.cfg
文件,和一个 MANIFEST.in
文件。文件结构如下:
1 2 3 4 5 6 7 8 9 10 |
project/ pyproject.toml setup.cfg MANIFEST.in regressor/ __init__.py data.json model.pickle predict.py train.py |
我们将使用 setuptools
,因为它已成为这项任务的标准。pyproject.toml
文件用于指定 setuptools
:
1 2 3 |
[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" |
关键信息在 setup.cfg
中提供。我们需要指定模块的名称、版本、一些可选描述、要包含的内容以及要依赖的内容,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[metadata] name = mlm_demo version = 0.0.1 description = a simple linear regression model [options] packages = regressor include_package_data = True python_requires = >=3.6 install_requires = scikit-learn==1.0.2 numpy>=1.22, <1.23 h5py |
MANIFEST.in
仅用于指定我们需要包含的额外文件。对于不包含非 Python 脚本的项目,可以省略此文件。但在我们的例子中,我们需要包含训练好的模型和数据文件:
1 2 |
include regressor/data.json include regressor/model.pickle |
然后,在项目目录中,我们可以通过以下命令将其作为模块安装到我们的 Python 系统中:
1 |
pip install . |
之后,以下代码将在**任何地方**工作,因为 regressor
是我们 Python 安装中可访问的模块:
1 2 3 4 5 6 |
import numpy as np from regressor.predict import predict X = np.asarray([[0.186,0,8.3,0,0.62,6.2,58,1.96,6,400,18.1,410,11.5]]) y = predict(X) print(y[0]) |
setup.cfg
中有几个细节值得解释:metadata
部分是为 pip
系统准备的。因此,我们将包命名为 mlm_demo
,您可以在 pip list
命令的输出中看到它。然而,Python 的模块系统会将模块名识别为 regressor
,这在 options
部分中指定。因此,这就是您应该在 import
语句中使用的名称。通常,为了方便用户,这两个名称是相同的,这就是为什么人们可以互换使用“包”和“模块”这两个词。同样,版本 0.0.1 出现在 pip
中,但代码不知道它。这是将此放入模块目录中的 __init__.py
的约定,因此您可以在使用它的另一个脚本中检查版本:
1 |
__version__ = '0.0.1' |
options
部分中的 install_requires
是使我们的项目运行的关键。这意味着当我们安装这个模块时,我们也需要安装那些其他模块以及它们指定的版本(如果已指定)。这可能会创建一个依赖树,但当您运行 pip install
命令时,pip
会处理它。正如您所料,我们使用 Python 的比较运算符 ==
来指定特定版本。但如果我们能接受多个版本,我们就会用逗号 (,
) 来分隔条件,就像上面的 numpy
的情况一样。
现在您可以将整个项目目录分发给他人(例如,以 ZIP 文件的形式)。他们可以在项目目录中用 pip install
进行安装,然后运行您的代码,命令行格式为 python -m regressor
,并提供适当的命令行参数。
最后说明一下:您可能听说过 Python 项目中的 requirements.txt
文件。它只是一个文本文件,通常放在包含 Python 模块或某些 Python 脚本的目录中。它的格式与上面提到的依赖项规范类似。例如,它可能看起来像这样:
1 2 3 |
scikit-learn==1.0.2 numpy>=1.22, <1.23 h5py |
其目的是您**不想**将您的项目打包成一个包,但仍然想提供您的项目所期望的库及其版本的信息。pip
可以理解这个文件,我们可以让它设置我们的系统来为项目做准备:
1 |
pip install -r requirements.txt |
但这只是针对开发中的项目,而这正是 requirements.txt
能够提供的所有便利。
为您的项目使用 venv
以上可能是分发和部署项目的最有效方式,因为它只包含最必要的文件。这也是推荐的方式,因为它与平台无关。如果我们更改 Python 版本或迁移到不同的操作系统(除非某些特定依赖项禁止),这仍然有效。
但在某些情况下,我们可能希望为我们的项目提供一个完全相同的运行环境。例如,我们不要求安装某些包,而是要求**不得**安装某些包。此外,还有一些情况是,当我们用 pip
安装了一个包之后,另一个包安装后,版本依赖关系就会中断。我们可以通过 Python 的 venv
模块来解决这个问题。
venv
模块来自 Python 的标准库,允许我们创建一个**虚拟环境**。它不是虚拟机或 Docker 等虚拟化技术提供的;相反,它极大地修改了 Python 操作的路径位置。例如,我们可以在我们的操作系统上安装多个 Python 版本,但虚拟环境总是假定 python
命令指的是特定版本。另一个例子是,在一个虚拟环境中,我们可以运行 pip install
来在虚拟环境目录中设置一些包,这些包不会干扰外部系统。
要开始使用 venv
,我们可以找到一个合适的位置并运行命令:
1 |
$ python -m venv myproject |
然后会创建一个名为 myproject
的目录。虚拟环境应该在 shell 中操作(以便可以操纵环境变量)。要**激活**虚拟环境,我们执行激活 shell 脚本,命令如下(例如,在 Linux 和 macOS 的 bash 或 zsh 下):
1 |
$ source myproject/bin/activate |
之后,您就进入了 Python 虚拟环境。python
命令将是您在虚拟环境中创建的命令(如果您在操作系统上安装了多个 Python 版本)。安装的包将位于 myproject/lib/python3.9/site-packages
下(假设是 Python 3.9)。当您运行 pip install
或 pip list
时,您只会看到虚拟环境下的包。
要退出虚拟环境,我们在 shell 命令提示符下运行 **deactivate**:
1 |
$ deactivate |
这被定义为一个 shell 函数。
如果您有多个正在开发的与项目,并且它们需要不同版本的包(例如不同版本的 TensorFlow),那么使用虚拟环境特别有用。您可以简单地创建一个虚拟环境,激活它,然后使用 pip install
命令安装所有必需库的正确版本,然后将项目代码放在虚拟环境目录中。您的虚拟环境目录的大小可能非常大(例如,仅安装 TensorFlow 及其依赖项就会占用近 1GB 的磁盘空间)。但之后,将整个虚拟环境目录分发给他人可以保证为您执行代码提供确切的环境。如果您不想运行 Docker 服务器,这可以作为 Docker 容器的替代方案。
进一步阅读
确实,还有一些其他工具可以帮助我们整洁地部署我们的项目。上面提到的 Docker 是其中之一。Python 标准库中的 zipapp
包也是一个有趣的工具。如果您想深入了解,以下是一些相关资源:
文章
- Python 教程,第 6 章,模块
- 分发 Python 模块
- 如何打包你的 Python 代码
- StackOverflow 上关于各种 venv 相关包的提问
API 和软件
- Setuptools
- Python 标准库的venv
总结
在本教程中,您已经了解了如何自信地完成项目并将其交付给用户运行。具体来说,您学习了:
- 将一个 Python 脚本文件夹转换为模块的最小更改
- 如何将模块转换为供
pip
使用的包 - 什么是 Python 中的虚拟环境,以及如何使用它
暂无评论。