转自:https://zhuanlan.zhihu.com/p/460233022,仅作学习记录。
平常我们习惯了使用 pip 来安装一些第三方模块,这个安装过程之所以简单,是因为模块开发者为我们默默地为我们做了所有繁杂的工作,而这个过程就是 打包
。
打包,就是将你的源代码进一步封装,并且将所有的项目部署工作都事先安排好,这样使用者拿到后即装即用,不用再操心如何部署的问题(如果你不想对照着一堆部署文档手工操作的话)。
你是否也想写一个python库,想向社会开源或者企业内部使用,想让别人通过简单的命令pip install
安装你的库。那么setuptools
绝对是最好用的python打包与分发工具
。
setuptools库的前身是distutils
(一个python标准库),setuptools本身不是标准库,所以需要自行安装。
setuptools提供的主要的功能有:
首先python库的打包分发方式有两种:源码包source dist(简称sdist)
、二进制包binary dist(简称bdist)
。
Python 包的分发可以分为两种:
跨平台的
,由于每次安装都要进行编译,相对二进包安装方式来说安装速度较慢。由于不同平台的编译出来的包无法通用,所以在发布时,需事先编译好多个平台的包
。Egg 格式
是由 setuptools 在 2004 年引入,而 Wheel 格式是由 PEP427 在 2012 年定义。Wheel 的出现是为了替代 Egg,它的本质是一个zip包
,其现在被认为是 Python 的二进制包的标准格式
。
以下是 Wheel 和 Egg 的主要区别:
wheel 包可以通过 pip 来安装,只不过需要先安装 wheel 模块,然后再使用 pip 的命令。
$ pip install wheel
$ pip wheel --wheel-dir=/local/wheels pkg
源码包sdist
就是我们熟悉的 .zip 、.tar.gz 等后缀文件。就是一个压缩包,里面包含了所需要的的所有源码文件以及一些静态文件(txt文本、css、图片等)。
$ python setup.py sdist --formats=gztar
setup.py后面会介绍,总之setup.py指定了打包分发的配置信息。
--formats
参数用来指定压缩格式,若不指定format格式,那么 sdist 将根据当前平台创建默认格式。在类 Unix 平台上,将创建后缀名为.tar.gz
分发包,而在Windows上为 .zip
文件。
执行完该命令,我们可以看到文件夹下多了dist文件夹
(包含压缩源码的分发包)和egg-info文件夹
(中间临时配置信息)。
安装源码包有两种方法,先解压缩源码包,或者直接安装源码包。
$ python setup.py install
等价于
$ python setup.py build
$ python setup.py install
'''
python setup.py install包括两步:python setup.py build python setup.py install。
这两步可分开执行, 也可只执行python setup.py install, 因为python setup.py install总是会先build后install.
'''
$ pip install xxx.zip
如果是开发阶段
,可以用下面两个命令,该命令不会真正的安装包
,而是在系统环境中创建一个软链接指向包实际所在目录
。 这样在修改包之后不用再安装就能生效,便于调试
。
$ pip install -e .
等价于
$ python setup.py develop
或者
$ python setup.py build develop
python目前主流的二进制包
格式是wheel(.whl后缀)
,它的前身是egg。wheel本质也还是一个压缩包,可以像像zip一样解压缩。与源码包相比,二进制包的特点是不用再编译
,也就是安装更快!在使用wheel之前,需要先安装wheel:$ pip install wheel
。
和 sdist 一样,可以通过 formats 参数
指定包格式。如:
$ python setup.py bdist --formats=rpm
等价于
$ python setup.py build_rpm
此外setuptools还提供了其他命令,起到和formats参数一样的效果,如下:
直接pip就可以了
$ pip install xxx.whl
上面我们讲述了python打包分发的两种方法,很容易发现整个打包过程最重要的就是setup.py
,它指定了重要的配置信息
。setup.py的内容如下:
from setuptools import setup
setup(
name = 'myapp',
…………
)
可见最关键的就是setuptools.setup这个函数,由它来控制打包分发
,包含以下信息:
from setuptools import setup
def readme():
with open('README.md', encoding='utf-8') as f:
content = f.read()
return content
setup(
name = 'myapp', # 包名称
version = '1.0', # 版本
author = 'xxx', # 作者
author_email = '[email protected]', # 作者邮箱
description='a example for pack python', # 描述
long_description=readme(), # 长文描述
long_description_content_type='text/markdown', # 长文描述的文本格式
keywords='pack', # 关键词
url='https://github.com/lihua/myapp', # 项目主页
classifiers=[ # 包的分类信息,见https://pypi.org/pypi?%3Aaction=list_classifiers
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
],
packages=find_packages(),
include_package_data=True,
license='Apache License 2.0', # 许可证
)
通过setup函数
的这些参数packages
、include_package_data
(其实就是MANIFEST.in文件
)、exclude_package_data
、package_data
、data_files
来指定需要打包的文件。
包含的文件如下:
packages参数
就是用来指示打包分发时需要包含的package
,type为list[str]
。
举个例子:
└── D:\workplace\python\pack_test
├──setup.py
├──debug
│ ├──debug.py
├──src
│ ├──__init__.py
│ ├──pack1
│ ├──__init__.py
│ ├──main.py
│ ├──config.txt
│ ├──data
│ │ ├──main.py
│ │ ├──a.dat
其中setup.py文件内容为
from setuptools import setup
setup(packages=['src'])
运行$ python setup.py sdist --formats=zip
打包命令后,发现只打包了src和旗下的__init__.py,pack1不见踪影。
├──src
│ ├──__init__.py
说明packages参数时,
不会递归的打包子package!只打包当前package!
改为 setup(packages=['src','src.pack1','src.pack1.data']) 这样就可以了。
但每次都这样写也太费时费力了。所以setuptools提供了两个函数find_namespace_packages
, find_packages
来快速找到所有的package。
首先明白一点,python中的packages有两种,
包含__init__.py的文件夹
(姑且叫做普通package
),不含__init__.py的文件夹
(这是python3引入的Namespace Packages命名空间包
)。改为 setup(packages=find_packages()) 发现没有打包data和debug文件夹!
原来是因为
find_packages只会打包内含__init__.py的package
,而data和debug文件夹正好没有__init__.py。
改为setup(packages=find_namespace_packages()) 就可以打包data和debug文件夹。
此外如果不想打包debug文件夹,可以给find_namespace_packages传递参数以指定在哪个文件夹下进行搜索,比如
setup(packages=find_namespace_packages('src'))
,这样就不会打包debug文件夹。
上面这些例子中都没有包含非源码文件
(如.dat和.txt文件),需要通过别的参数include_package_data
(其实就是http://MANIFEST.in文件)、exclude_package_data、package_data
来打包非源码文件。
include_package_data
是bool
类型,默认值为True
。当为True时,将根据http://MANIFEST.in文件来打包分发库。
http://MANIFEST.in文件指定了一些语法规则,主要是用来打包非源码文件的,语法规则如下:
还是以之前的例子为例,加入http://MANIFEST.in文件
└── D:\workplace\python\pack_test
├──setup.py
├──MANIFEST.in
├──debug
│ ├──debug.py
├──src
│ ├──__init__.py
│ ├──pack1
│ ├──__init__.py
│ ├──main.py
│ ├──config.txt
│ ├──data
│ │ ├──main.py
│ │ ├──a.dat
http://MANIFEST.in文件内容为:
recursive-include . *.txt *.dat # 递归遍历当前文件夹,找到符合*.dat、*.txt的文件
或者
include src/pack1/*.txt
include src/pack1/data/*.dat
除了通过http://MANIFEST.in的方法来指定,还可以通过package_data参数
来指定,这边建议还是统一用http://MANIFEST.in文件的方式,免得造成不一致性
。
# setup.py
from setuptools import setup
setup(package_data={'':['*.txt'],'src.pk1':['*.dat']} # 其中''表示所有文件夹下
顾名思义就是去除文件
exclude_package_data={'src.pk1':['*.txt']}
一个项目库可能会依赖于很多其他库,比如我们安装pandas,该库依赖于numpy。那我们用pip conda这些命令安装时,从来不用操心哪些依赖包需要安装,它们的版本限制是怎么样的,而这些信息是setuptools打包分发库时就确定的
。所以当setuptools打包分发库时,要指定依赖包有哪些,它们又有什么限制。
针对依赖包安装与版本管理这项功能,setup函数提供了一些参数install_requires
、 setup_requires
、tests_require
、extras_require
。
from setuptools import setup, find_packages
setup(
...
# 表明当前模块依赖哪些包,若环境中没有,则会从pypi中自动下载安装!!!
install_requires=['docutils>=0.3'],
# setup.py 本身要依赖的包,这通常是为一些setuptools的插件准备的配置,这里列出的包,不会自动安装。
setup_requires=['pbr'],
# 仅在测试时需要使用的依赖,在正常发布的代码中是没有用的。
# 在执行python setup.py test时,可以自动安装这三个库,确保测试的正常运行。
tests_require=[
'pytest>=3.3.1',
'pytest-cov>=2.5.1',
],
# install_requires 在安装模块时会自动安装依赖包
# 而 extras_require 不会,这里仅表示该模块会依赖这些包
# 但是这些包通常不会使用到,只有当你深度使用模块时,才会用到,这里需要你手动安装
extras_require={
'PDF': ["ReportLab>=1.2", "RXP"],
'reST': ["docutils>=0.3"],
}
)
关于 install_requires
, 有以下四种常用的表示方法:
有时我们也需要对python的版本进行限制,可以通过参数python_requires
来实现。
setup(python_requires='>=2.7, <=3')
有时候我们的库包含了一些非常重要的功能,每次都提供python XXX.py来运行不太方便,最好是把脚本放入系统环境path,以命令行的形式来执行。比如tensorRT就提供了trtexec命令。
那么setup函数提供了entry_points
和scripts
这两个参数。它们的区别在于:
把python文件中的函数自动生成为可执行脚本
把.sh、.py等可执行脚本生成到系统path中
from setuptools import setup
setup(
…………
# 把python中的函数自动生成为一个可执行的脚本
# 如下:把fool.main文件中的main函数自动生成为一个可执行脚本,可以通过命令foo执行该脚本
entry_points={
'console_scripts': [ # key值为console_scripts
'foo = foo.main:main' # 格式为'命令名 = 模块名:函数名'
]
},
# 将 bin/foo.sh 和 bar.py 脚本,生成到系统 PATH中
# 执行 python setup.py install 后
# 会生成 如 /usr/bin/foo.sh 和 如 /usr/bin/bar.py
scripts=['bin/foo.sh', 'bar.py']
)
python开发成本低,但运行速度堪忧,c/c++速度无人能敌,但内存管理、模板编程等对程序猿来说,开发成本太高。那么python+c的混合编程
的解决方案应运而生,博采众长,python就像前端,简单的语言赏心悦目,c就像后端,适合做计算密集型任务。而且c/c++还可以有效规避掉python的GIL锁,速度更上一层楼。
python+c/c++混合编程的技术路径有很多,如:
本文还是主要介绍setuptools是如何进行c/c++ 扩展。
编译c/c++拓展源码的命令为:
python setup.py build_ext --inplace
。
或者直接python setup.py build
该命令包括了build_ext步骤。
那么我们该如何指导编译器编译c/c++源码呢。
本质上setuptools是根据setup.py配置来指导生成gcc命令行,当然你也可以粗暴地直接用gcc命令行来编译c/c++拓展源码,但工程量太大,setuptools支持很多混合编程技术cython、SWIG等等。所以甭管你采用什么混合编程技术,绕不开setuptools。setuptools编译c/c++拓展源码的过程主要是把源代码编译成动态连接库
(linux下是.so,windows下是.pyd)。这样就可以在.py中愉快import并使用拓展模块了。
主要看setup函数的ext_modules参数
,该参数type为list[setuptools.Extension]
。所以编译核心就在于这个setuptools.Extension类
,该类只支持c/c++拓展
,要实现cuda拓展需要自定义Extension类
,如pytorch的CUDAExtension
。
setuptools.Extension类有几个重要的构造参数(详见API文档)
举个例子:
from setuptools import setup,Extension
setup(
ext_modules=[
Extension(
name='foo', # type=str。并且还支持层级命名,如myapp.foo
sources=['foo/csrc/foo1.c','foo/csrc/foo2.c'], # type=list[str]。源代码的文件名,可以用glob.glob查找所有.c文件
include_dirs=['foo'], # type=list[str]。拓展include头文件,相当于传递给gcc -I
)]
)
setuptools.Extension用define_macros 和 undef_macros构造参数来定义或取消定义宏。define_macros的type为list[tuple( name:str , value:str|None )]
。值为 None 的宏 FOO
等价于#define FOO
,否则等价于# define FOO value值
。undef_macros 同理,等价于#undef FOO
。
举个例子:陈鸣:来编写你的 setup 脚本(二)
Extension(define_macros=[('NDEBUG', '1'),('HAVE_STRFTIME', None)],
undef_macros=['HAVE_FOO', 'HAVE_BAR'])
上面的代码相当于在每个C文件前加上了:
#define NDEBUG 1
#define HAVE_STRFTIME
#undef HAVE_FOO
#undef HAVE_BAR
自定义命令行为是setuptools进阶知识。setuptools包括许多命令,如下:
Standard commands:
build build everything needed to install
build_py "build" pure Python modules (copy to build directory)
build_ext build C/C++ extensions (compile/link to build directory)
build_clib build C/C++ libraries used by Python extensions
build_scripts "build" scripts (copy and fixup #! line)
clean clean up temporary files from 'build' command
install install everything from build directory
install_lib install all Python modules (extensions and pure Python)
install_headers install C/C++ header files
install_scripts install scripts (Python or otherwise)
install_data install data files
sdist create a source distribution (tarball, zip file, etc.)
register register the distribution with the Python package index
bdist create a built (binary) distribution
bdist_dumb create a "dumb" built distribution
bdist_rpm create an RPM distribution
bdist_wininst create an executable installer for MS Windows
upload upload binary package to PyPI
Extra commands:
见:https://pythonhosted.org/an_example_pypi_project/setuptools.html
这些命令具体是由定义在setuptools.command
中的类执行的。比如python setup.py bdist
由setuptools.command.bdist类
来执行。因此我们可以继承于setuptools.command
中的类来执行自定义的命令行为。
比如pytorch的Build.Extension
就继承于setuptools.command.build_ext
。具体怎么继承并改写这个command类就需要阅读源码
了。
继承完command类后,需要通过cmdclass参数
告诉setuptools,该参数为一个字典
,key为str命令名,value为继承于command类。
from setuptools import setup
import setuptools.command.build_ext as build_ext
class BuildExtension(build_ext,object):
…………
setup( cmdclass={'build_ext': BuildExtension} )