关于部署Python项目的第一课
总结在本教程中,你已经看到了我们如何自信地包装我们的项目并将其交付给另一个用户运行。具体来说,你学到了:
对Python脚本文件夹的最小改动,使其成为一个模块
如何将一个模块转换为 pip 的包
什么是 Python 中的虚拟环境,以及如何使用它
作者:Adrian Tam 日期:2022年4月20日 在 机器学习的Python中
最后更新于2022年5月4日
在用Python开发了一个项目之后,我们希望与其他人分享我们的项目。它可以是你的朋友,也可以是你的同事。也许他们对你的代码不感兴趣,但他们想运行它,并对它进行一些真正的利用。例如,你创建了一个回归模型,可以根据输入特征预测一个值。你的朋友想提供他们自己的特征,看看你的模型预测了什么值。但是随着你的Python项目越来越大,它就不像给你的朋友发送一个小脚本那么简单了。可能有许多支持文件,多个脚本,还有对一系列库的依赖性。把所有这些都做好可能是一个挑战。
完成本教程后,你将学会。
如何使你的代码成为一个模块,使其更容易部署
如何为你的模块创建一个包,以便我们可以依靠pip来管理依赖关系
如何使用venv模块来创建可重复的运行环境
让我们开始吧!
概述
本教程分为四个部分;它们是:
从开发到部署
创建模块
从模块到软件包
为你的项目使用venv
从开发到部署
当我们在Python中完成一个项目时,偶尔,我们不想搁置它,而是想让它成为一项常规工作。我们可能会完成一个机器学习模型的训练,并积极使用训练好的模型进行预测。我们可能会建立一个时间序列模型并将其用于下一步的预测。然而,每天都有新的数据进来,所以我们需要重新训练它,以适应发展,保持未来预测的准确性。
不管是什么原因,我们需要确保程序能按预期运行。然而,这可能比我们想象的要难。一个简单的Python脚本可能不是一个困难的问题,但随着我们的程序越来越大,依赖性越来越强,很多事情都可能出错。例如,我们使用的一个库的较新版本会破坏工作流程。或者我们的Python脚本可能会运行一些外部程序,而在我们的操作系统升级之后,这些程序可能就不再工作了。另一种情况是,程序依赖于位于特定路径的一些文件,但我们可能不小心删除或重命名了一个文件。
我们的程序总是有办法无法执行。但我们有一些技术可以使它更健壮、更可靠。
创建模块
在上一篇文章中,我们演示了可以用以下命令检查一个代码片段的完成时间。
python -m timeit -s 'import numpy as np' 'np.random.random()'
#At the same time, we can also use it as part of a script and do the following:
import timeit
import numpy as np
time = timeit.timeit("np.random.random()", globals=globals())
print(time)
Python 中的 import 语句允许你通过把它看作一个模块来重用另一个文件中定义的函数。你可能想知道我们如何使一个模块不仅提供函数,而且成为一个可执行的程序。这是帮助部署我们的代码的第一步。如果我们能使我们的模块成为可执行程序,用户就不需要了解我们的代码是如何结构化的来使用它。
如果我们的程序大到有多个文件,最好把它打包成一个模块。Python中的模块通常是一个Python脚本的文件夹,有一个明确的入口。因此,它更方便发送给其他人,也更容易理解其流程。此外,我们可以给模块添加版本,让 pip 跟踪所安装的版本。
一个简单的、单文件的程序可以写成以下样子。
import random
def main():
n = random.random()
print(n)
if __name__ == "__main__":
main()
如果我们在本地目录下将其保存为 randomsample.py,我们可以用以下方式运行它。
python randomsample.py
#或:
python -m randomsample
我们还可以在另一个脚本中重复使用这些函数。
import randomsample
randomsample.main()
这样做的原因是,只有当脚本作为主程序运行时,神奇的变量name才是"main",而当从另一个脚本导入时则不是。有了这个,你的机器学习项目大概可以打包成以下样子。
regressor/
__init__.py
data.json
model.pickle
predict.py
train.py
现在,regressor 是一个包含这五个文件的目录。而 init.py 是一个空文件,只是为了表明这个目录是一个可以导入的 Python 模块。脚本 train.py 的内容如下
import os
import json
import pickle
from sklearn.linear_model import 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的脚本是。
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保存模型。
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保存模型。
import pickle
from regressor.train import train
model = train()
with open("model.pickle", "wb") as fp:
pickle.save(model, fp)
import pickle
from regressor.train import train
model = train()
with open("model.pickle", "wb") as fp:
pickle.save(model, fp)
如果我们把这个pickle文件移到regressor/目录下,我们也可以在命令行中做如下操作来运行这个模型。
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,代码如下。
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])
然后我们可以直接从模块中运行该模型。
python -m regressor 0.186 0 8.3 0 0.62 6.2 58 1.96 6 400 18.1 410 11.5
注意上面的例子中.predict import predict的行式使用了Python的相对导入语法。这应该在一个模块内使用,以便从同一模块的其他脚本中导入组件。
project/
pyproject.toml
setup.cfg
MANIFEST.in
regressor/
__init__.py
data.json
model.pickle
predict.py
train.py
我们将使用setuptools,因为它已经成为这项任务的标准。文件 pyproject.toml 是为了指定 setuptools。
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
关键信息在setup.cfg中提供。我们需要指定模块的名称、版本、一些可选的描述、包括哪些内容、依赖哪些内容,比如下面这样。
[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脚本的项目中,这个文件可以被省略。但在我们的案例中,我们需要包括训练好的模型和数据文件。
include regressor/data.json
include regressor/model.pickle
然后在项目目录中,我们可以用以下命令将其作为模块安装到我们的Python系统中。
pip install .
之后,下面的代码在任何地方都可以工作,因为regressor是我们Python安装中可以访问的模块。
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中,有几个细节值得解释:元数据部分是为管道系统准备的。因此,我们将我们的包命名为 mlm_demo,你可以在 pip list 命令的输出中看到它。然而,Python 的模块系统会将模块名称识别为选项部分中指定的 regressor。因此,这是你在导入语句中应该使用的名字。通常,为了方便用户,这两个名字是一样的,这就是人们交替使用 "包 "和 "模块 "这两个名字的原因。同样,0.0.1版本出现在pip中,但从代码中无法得知。一个惯例是把它放在模块目录下的 init.py 中,这样你就可以在另一个使用它的脚本中检查版本。
version = '0.0.1'。
选项部分的 install_requires 部分是使我们的项目运行的关键。它意味着当我们安装这个模块时,我们也需要安装那些版本的其他模块(如果指定的话)。这可能会产生一棵依赖树,但当你运行pip安装命令时,pip会处理好这个问题。正如你所期望的,我们使用Python的比较运算符==来表示一个特定的版本。但是如果我们可以接受多个版本,我们就用逗号(,)来分隔这些条件,比如上面numpy的情况。
现在你可以将整个项目目录运送给其他人(例如,在一个ZIP文件中)。他们可以在项目目录下用pip install进行安装,然后用python -m regressor运行你的代码,并提供适当的命令行参数。
最后一点:也许你听说过 Python 项目中的 requirements.txt 文件。它只是一个文本文件,通常与 Python 模块或一些 Python 脚本放在一个目录中。它的格式类似于上面提到的依赖性规范。例如,它可能看起来像这样。
scikit-learn==1.0.2
numpy>=1.22, <1.23
h5py
其目的是,你不想把你的项目变成一个包,但仍然想对你的项目所期望的库和它们的版本进行提示。这个文件可以被pip理解,我们可以让它设置我们的系统,为项目做准备。
pip install -r requirements.txt
但这只是针对开发中的项目,requirements.txt所能提供的便利也就这些了。
为你的项目使用venv
上面的方法可能是最有效的运送和部署项目的方法,因为你只包括最重要的文件。这也是推荐的方式,因为它是平台无关的。如果我们改变我们的 Python 版本或转移到一个不同的操作系统,这仍然有效(除非某些特定的依赖关系禁止我们这样做)。
但在有些情况下,我们可能想为我们的项目运行重现一个确切的环境。例如,我们不要求安装某些软件包,而是希望有些软件包必须不安装。另外,有些情况下,我们用pip安装了一个包后,在安装另一个包后,版本依赖关系就会中断。我们可以用Python中的venv模块来解决这个问题。
venv模块来自Python的标准库,允许我们创建一个虚拟环境。它不是像Docker可以提供的虚拟机或虚拟化;相反,它严重地修改了Python操作的路径位置。例如,我们可以在操作系统中安装多个版本的Python,但虚拟环境总是假设python命令意味着一个特定的版本。另一个例子是,在一个虚拟环境中,我们可以运行 pip install 来在虚拟环境目录中设置一些软件包,这样就不会影响到外面的系统。
要开始使用venv,我们可以简单地找到一个好的位置并运行命令。
$ python -m venv myproject
然后会有一个名为myproject的目录被创建。一个虚拟环境应该在shell中运行(所以环境变量可以被操作)。为了激活一个虚拟环境,我们用下面的命令执行激活的shell脚本(例如,在Linux和macOS的bash或zsh下)。
$ source myproject/bin/activate
而后,你就在Python虚拟环境下。python 命令将是你在虚拟环境中创建的命令(如果你的操作系统中安装了多个 Python 版本的话)。而安装的软件包将位于 myproject/lib/python3.9/Site-packages 下(假设是 Python 3.9)。当你运行 pip install 或 pip list 时,你只能看到虚拟环境下的软件包。
要离开虚拟环境,我们在shell命令行中运行deactivate。
$ deactivate
这被定义为一个shell函数。
如果你有多个项目在开发中,并且它们需要不同版本的包(例如不同版本的TensorFlow),使用虚拟环境可能特别有用。你可以简单地创建一个虚拟环境,激活它,使用pip install命令安装你需要的所有库的正确版本,然后把你的项目代码放在虚拟环境里面。
你的虚拟环境目录的大小可能是巨大的(例如,仅仅安装TensorFlow及其依赖项就会消耗几乎1GB的磁盘空间)。但之后,将整个虚拟环境目录运送给其他人,可以保证执行你的代码的确切环境。如果你不喜欢运行Docker服务器,这可以作为Docker容器的一个替代方案。
进一步阅读
事实上,还有一些其他的工具可以帮助我们整齐地部署我们的项目。上面提到的Docker可以是一个。Python标准库中的zipapp包也是一个有趣的工具。如果你想深入了解,下面是关于这个主题的资源。
文章
Python教程,第6章,模块
分发Python模块
如何打包你的Python代码
StackOverflow上关于各种venv相关软件包的问题
API和软件
设置工具
来自Python标准库的venv