一文教会你正确打包Python程序
Python 是一门强大的编程语言。它最大的缺点之一就是打包。在 Python 社区里,这是个众所周知的事实。虽然在过去几十年,安装、导入、创建包的过程已经改善了,但是仍然不能和从 Python 的缺点中吸取了很多教训的 Go 和 Rust 这样的新语言,以及其他更加成熟的语言相媲美。
在本教程中,你将会学到创建以及分享自己的包所需要的一切知识。想要了解关于 Python 包的背景知识,请阅读如何使用 Python 包。
项目打包
打包一个项目,指的是你创建一系列条理清晰的 Python 模块,可能还有其他文件,并且把它们放到一个很容易使用的结构当中去的过程。有一些不得不考虑的地方,比如对其他包的依赖,内部结构(子包),版本控制,目标用户和包的形式(源文件还是二进制文件)。
示例
让我们从一个简单的例子开始。conman 是一个用来管理配置的包。它支持各种文件格式,还支持使用 etcd 进行分布式配置。
一个包的内容通常会保存在一个目录下(尽管通常也会把子包分离在不同的文件目录下),并且有时候会被放在自己的 git 仓库下,就像本文中这样。
根目录包含了各种各样的配置文件(setup.py
是一个必须的并且最重要的配置文件),代码本身通常在一个子目录下,目录名就是包的名称。当然最好还有一个测试文件目录。。下面就是 “conman” 的目录结构:
> tree . ├── LICENSE ├── MANIFEST.in ├── README.md ├── conman │ ├── __init__.py │ ├── __pycache__ │ ├── conman_base.py │ ├── conman_etcd.py │ └── conman_file.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tests │ ├── __pycache__ │ ├── conman_etcd_test.py │ ├── conman_file_test.py │ └── etcd_test_util.py └── tox.ini
让我们快速地看一眼 setup.py 文件。它从 setuptools 包中导入了两个函数:setup()
和 find_packages()
。然后调用了 setup()
,并且将 find_packages()
作为其中的一个参数。
from setuptools import setup, find_packages setup(name='conman', version='0.3', url='https://github.com/the-gigi/conman', license='MIT', author='Gigi Sayfan', author_email='the.gigi@gmail.com', description='Manage configuration files', packages=find_packages(exclude=['tests']), long_description=open('README.md').read(), zip_safe=False, setup_requires=['nose>=1.0'], test_suite='nose.collector')
这是非常普通的配置。setup.py
是一个常规的 Python 文件,你可以在文件里面做任何你想做的事情,不过它的主要工作是用适当的参数去调用 setup()
函数。因为在安装你的包的时候,setup()
函数将会被各种各样的工具以标准的方法调用。在下一节,我将会介绍一些细节。
配置文件
除了 setup.py
,还有一些其他可选的配置文件,在这里罗列出来,一并介绍一下它们各自的使用目的。
Setup.py
setup()
函数会有很多的命名参数,用来控制包安装的方方面面,并可运行不同的命令。许多的参数指定了在上传包到代码库时搜索和过滤所用到的元数据。
- name:包的名称(以及如何在 PYPI 上呈现)
- version:这对于保持适当的依赖关系至关重要
- url:包的链接,通常为 Github 上的链接,或者是 readthedocs 链接
- packages:需要包含的子包列表,
find_packages()
将帮助我们查找 - setup_requires:指定依赖项
- test_suite:测试时运行的工具
long_description
在这里设置为 README.md
文件的内容,这是一项最佳实践,只在一个来源处做说明介绍。
Setup.cfg
setup.py 文件还提供了一个命令行界面来运行各种命令。例如:运行单元测试,你可以输入:python setup.py test
running test running egg_info writing conman.egg-info/PKG-INFO writing top-level names to conman.egg-info/top_level.txt writing dependency_links to conman.egg-info/dependency_links.txt reading manifest file 'conman.egg-info/SOURCES.txt' reading manifest template 'MANIFEST.in' writing manifest file 'conman.egg-info/SOURCES.txt' running build_ext test_add_bad_key (conman_etcd_test.ConManEtcdTest) ... ok test_add_good_key (conman_etcd_test.ConManEtcdTest) ... ok test_dictionary_access (conman_etcd_test.ConManEtcdTest) ... ok test_initialization (conman_etcd_test.ConManEtcdTest) ... ok test_refresh (conman_etcd_test.ConManEtcdTest) ... ok test_add_config_file_from_env_var (conman_file_test.ConmanFileTest) ... ok test_add_config_file_simple_guess_file_type (conman_file_test.ConmanFileTest) ... ok test_add_config_file_simple_unknown_wrong_file_type (conman_file_test.ConmanFileTest) ... ok test_add_config_file_simple_with_file_type (conman_file_test.ConmanFileTest) ... ok test_add_config_file_simple_wrong_file_type (conman_file_test.ConmanFileTest) ... ok test_add_config_file_with_base_dir (conman_file_test.ConmanFileTest) ... ok test_dictionary_access (conman_file_test.ConmanFileTest) ... ok test_guess_file_type (conman_file_test.ConmanFileTest) ... ok test_init_no_files (conman_file_test.ConmanFileTest) ... ok test_init_some_bad_files (conman_file_test.ConmanFileTest) ... ok test_init_some_good_files (conman_file_test.ConmanFileTest) ... ok ---------------------------------------------------------------------- Ran 16 tests in 0.160s OK
setup.cfg 是一个 ini 格式的文件,可能包含传递给 setup.py
的命令的可选默认值。在这里,setup.cfg 中包含了 nosetests
(刚才运行的单元测试)的一些选项:
[nosetests] verbose=1 nocapture=1
MANIFEST.in
此文件中包含有不属于内部包目录,但你仍想纳入进来的文件。这些文件通常是 readme
文件,license 文件以及一些类似的文件。其中,比较重要的一个是 requirements.txt
。 pip 使用该文件安装其他必须的包。
下面是 conman 的 MANIFEST.in
文件:
include LICENSE include README.md include requirements.txt
依赖项
您可以在 setup.py
文件的 install_requires
部分和 requirements.txt
文件中指定依赖项。Pip 将会自动安装 install_requires
中列出的依赖项,而不是 requirements.txt
文件。要安装后者中指定的依赖项,在运行 pip 时必须明确指定:pip install -r requirements.txt
。
install_requires
选项旨在指定所要求模块的最低主版本号等较抽象的要求。而在 requirements.txt 文件的要求更加具体,通常细致到次版本号。
下面是 conman 的 requirements 文件。你可以看到,所有的版本都被定死了,这也就意味着,当所依赖的包中有一个升级了或者引入了让 conman 无法运行的变化,就会产生负面影响。
PyYAML==3.11 python-etcd==0.4.3 urllib3==1.7 pyOpenSSL==0.15.1 psutil==4.0.0 six==1.7.3
然而把版本号固定写死却提供了可预测性,并且这样会让人觉得心安。如果许多人在不同的时间安装软件包,这一点尤其重要。如果不固定下来,每个人在安 装包时将会得到不同版本的依赖。版本固定的缺点是,如果你跟不上依赖项的进度,你可能会被困在老化的版本上,可能表现不佳,甚至容易受攻击。
我是在 2014 年写的 conman,之后就没怎么管了。但是现在,为了写这篇教程,我更新了所有的东西,几乎每一个依赖项都有很大的改进。
发布
你可以创建一个源代码发布版或二进制发布版。两者我都会介绍。
源发布版
你使用 python setup.py sdist
这个命令创建源发布文件。下面是 conman 的输出:
> python setup.py sdist running sdist running egg_info writing conman.egg-info/PKG-INFO writing top-level names to conman.egg-info/top_level.txt writing dependency_links to conman.egg-info/dependency_links.txt reading manifest file 'conman.egg-info/SOURCES.txt' reading manifest template 'MANIFEST.in' writing manifest file 'conman.egg-info/SOURCES.txt' warning: sdist: standard file not found: should have one of README, README.rst, README.txt running check creating conman-0.3 creating conman-0.3/conman creating conman-0.3/conman.egg-info making hard links in conman-0.3... hard linking LICENSE -> conman-0.3 hard linking MANIFEST.in -> conman-0.3 hard linking README.md -> conman-0.3 hard linking requirements.txt -> conman-0.3 hard linking setup.cfg -> conman-0.3 hard linking setup.py -> conman-0.3 hard linking conman/__init__.py -> conman-0.3/conman hard linking conman/conman_base.py -> conman-0.3/conman hard linking conman/conman_etcd.py -> conman-0.3/conman hard linking conman/conman_file.py -> conman-0.3/conman hard linking conman.egg-info/PKG-INFO -> conman-0.3/conman.egg-info hard linking conman.egg-info/SOURCES.txt -> conman-0.3/conman.egg-info hard linking conman.egg-info/dependency_links.txt -> conman-0.3/conman.egg-info hard linking conman.egg-info/not-zip-safe -> conman-0.3/conman.egg-info hard linking conman.egg-info/top_level.txt -> conman-0.3/conman.egg-info copying setup.cfg -> conman-0.3 Writing conman-0.3/setup.cfg creating dist Creating tar archive removing 'conman-0.3' (and everything under it)
你会发现,我得到了一个关于未找到标准后缀 README 文件的警告,因为我喜欢使用 Markdown,所以我使用的是 “README.md” 。除此之外,所有的包源文件和附加文件都被包含进去。然后,在 conman.egg-info
目录下生成了一堆元数据。最后,一个名为 conman-0.3.tar.gz
的压缩文件被创建,并放到 dist
子目录当中。
安装该软件包也需要先进行构建(即使它是纯 Python )。你只需给出压缩包的路径,就能使用 pip 安装它。 例如:
pip install dist/conman-0.3.tar.gz Processing ./dist/conman-0.3.tar.gz Installing collected packages: conman Running setup.py install for conman ... done Successfully installed conman-0.3
Conman 被安装在 site-packages 文件夹下,可以和其他包一样导入:
import conman conman.__file__ '/Users/gigi/.virtualenvs/conman/lib/python2.7/site-packages/conman/__init__.pyc'
Wheels
Wheels 是打包 Python 代码和扩展 C 语言的一个相对新的方法。它们替换了 egg 格式。有好几种类型的 Wheels:纯 Python Wheels,平台 Wheels 以及通用 Wheels。像 conman 这样的纯 Python Wheels 包,没有任何 C 语言的扩展代码。
平台 wheels 包含 C 扩展代码。通用 wheels 是纯 Python Wheels,但同时兼容 Python 2 和 Python 3(它们甚至不需要 2to3 转换)。如果你有需要同时兼容 Python 2 和 Python 3 (这变得越来越重要)的纯 Python 包,你只需要构建一个通用 wheels 即可,不必分别构建 Python 2 wheels 和 Python 3 wheels。
如果你的软件包有 C 语言扩展的代码,你必须针对各个平台分别构建平台 Wheels。对于包含 C 扩展的包来说,构建平台 wheels 的好处巨大,因为不用在目标机器上安装编译器和支持库。Wheel 中已包含一个构建好了的的包,因此你知道肯定不会构建失败,而且安装速度更快,因为只需要拷贝一下即可。使用像 Numpy 和 Pandas 这样的科学计算库的的用户能真正体会到这一点的好处,因为安装这些包需要花费大量时间,并且一旦缺少某些库文件或者编译器配置不恰当,就会导致安装失败。
创建纯 Python 或者平台 Wheels 的命令是:python setup.py bdist_wheel
。
Setuptools (提供了 setup()
函数的引擎)会自动检测是需要纯 Python 还是平台 Wheel 。
running bdist_wheel running build running build_py creating build creating build/lib creating build/lib/conman copying conman/__init__.py -> build/lib/conman copying conman/conman_base.py -> build/lib/conman copying conman/conman_etcd.py -> build/lib/conman copying conman/conman_file.py -> build/lib/conman installing to build/bdist.macosx-10.9-x86_64/wheel running install running install_lib creating build/bdist.macosx-10.9-x86_64 creating build/bdist.macosx-10.9-x86_64/wheel creating build/bdist.macosx-10.9-x86_64/wheel/conman copying build/lib/conman/__init__.py -> build/bdist.macosx-10.9-x86_64/wheel/conman copying build/lib/conman/conman_base.py -> build/bdist.macosx-10.9-x86_64/wheel/conman copying build/lib/conman/conman_etcd.py -> build/bdist.macosx-10.9-x86_64/wheel/conman copying build/lib/conman/conman_file.py -> build/bdist.macosx-10.9-x86_64/wheel/conman running install_egg_info running egg_info creating conman.egg-info writing conman.egg-info/PKG-INFO writing top-level names to conman.egg-info/top_level.txt writing dependency_links to conman.egg-info/dependency_links.txt writing manifest file 'conman.egg-info/SOURCES.txt' reading manifest file 'conman.egg-info/SOURCES.txt' reading manifest template 'MANIFEST.in' writing manifest file 'conman.egg-info/SOURCES.txt' Copying conman.egg-info to build/bdist.macosx-10.9-x86_64/wheel/conman-0.3-py2.7.egg-info running install_scripts creating build/bdist.macosx-10.9-x86_64/wheel/conman-0.3.dist-info/WHEEL<br>
查看 dist
目录,你可以看到创建了一个纯 Python Wheel。
ls -la dist dist/ total 32 -rw-r--r-- 1 gigi staff 5.5K Feb 29 07:57 conman-0.3-py2-none-any.whl -rw-r--r-- 1 gigi staff 4.4K Feb 28 23:33 conman-0.3.tar.gz
文件名 “conman-0.3-py2-none-any.whl” 有几个组成部分:包名,包版本,Python 版本,平台版本,最后是扩展名 “whl”。
如需创建通用包,你只需要添加 --universal
选项,比如:python setup.py bdist_wheel --universal
。
生成的 Wheel 会被命名为:”conman-0.3-py2.py3-none-any.whl”。
请注意:如果你创建了通用型包,就要确保你的代码的确能兼容 Python 2 和 Python 3。
结语
编写自己的Python包要求使用许多工具,指定大量的元数据,并仔细考虑你的依赖项和目标受众。但回报也是巨大的。
如果你写出有用的代码并正确打包,人们将能够轻松地安装它,并从中受益。
本文文字及图片出自 www.codingpy.com
你也许感兴趣的:
- 【外评】Python 为何如此糟糕…
- 【外评】用 Python 解释 Rust 背后的思想或理念
- Python 版本之间的主要变化摘要
- 【外评】Python 与苹果应用商店的拒绝作斗争
- 【外评】使用不安全的 Python 将速度提高 100 倍
- 谷歌裁掉整个 Python 团队!PyTorch 创始人急得直骂人:“WTF!核心语言团队无可替换”
- 谷歌Python团队全员被裁——负责内部Python所有基础设施、曾对数亿行代码执行自动重构
- 【译文】Python–一种深受喜爱但永远存在缺陷的语言
- 再同意不过了
- 【译文】减轻 Python 打包的痛苦
你对本文的反应是: