# Python setup.py 的全面解读最早接触setup.py的时候是在一个老项目的根目录下当时凑合着看了几眼觉得就是个打包工具。后来碰到了一个奇怪的问题——装了两个版本的同一个库互相覆盖导致程序跑不起来折腾了一整天回头翻setup.py的文档才明白原来这里面藏着不少门道。它到底是什么setup.py本质上是一个Python脚本通常放在项目的最外层目录。它跟普通的py文件没什么两样核心作用就是调用setuptools或者distutils提供的setup函数。写个最简单的版本fromsetuptoolsimportsetup setup(namemyproject,version0.1.0,packages[myproject],)这段代码跑起来后Python解释器会在当前目录找对应名字的包然后生成一系列元数据信息。可以把setup.py理解成一个配置清单告诉Python工具链这项目叫什么名字、版本号多少、依赖哪些第三方库、入口函数在哪里。有意思的是setup.py本身也是用Python写的这就意味着可以在里面写逻辑判断。比如根据操作系统决定装不同的依赖或者从环境变量读取版本号。这种灵活性在某些复杂场景下特别有用不过也带来了可读性和可复现性方面的问题——这一点后面再细说。它能做什么设想要去朋友家做客得提前把自己收拾好带上该带的东西。setup.py干的活跟这个差不多——把写好的代码整理好告诉外界这个包里有什么、怎么用、需要什么。具体来说能干这几件事最基础的是安装。python setup.py install会把整个项目装到site-packages里之后在任意目录都能import。刚学Python那会儿最爱这么干后来发现这其实不是最佳方式。再一个是打包。python setup.py sdist会生成一个tar.gz的源码包python setup.py bdist_wheel生成wheel包。这两个文件就是发布到PyPI上的东西。打个比方写好的代码像是自己腌的咸菜打包就是把咸菜封装进玻璃罐贴上标签方便别人拿回去吃。还有开发环境安装。python setup.py develop或者用pip的-e .参数会在site-packages里创建一个链接指向当前代码目录。改代码不用重新装就能生效调试起来方便得多。最后一个容易被忽视的功能——生成元数据。python setup.py --name、python setup.py --version这些命令能直接读到setup函数里写的配置。有些自动化脚本会借助这个来获取项目信息。怎么使用基本用法其实不复杂项目根目录下直接敲命令python setup.pyinstallpython setup.py sdist python setup.py bdist_wheel python setup.py develop配置文件写起来也有套路。拿一个实际项目举例子fromsetuptoolsimportsetup,find_packages setup(namemyutils,version1.0.0,description一个常用工具集合,author作者名,author_emailemailexample.com,packagesfind_packages(),install_requires[requests2.20.0,click,],entry_points{console_scripts:[myutilsmyutils.cli:main,],},python_requires3.6,)这里有几个容易踩坑的地方。find_packages()默认会包含当前目录下所有带__init__.py的目录如果不小心把测试目录暴露出去用户装完包会发现多了些不该有的东西。解决方案是加上exclude参数packagesfind_packages(exclude[tests,tests.*]),还有一个是entry_points这个用来注册命令行入口。写了一个命令行工具之后通过这行配置用户装完包就能直接在终端敲myutils来运行不用满世界找脚本路径。最佳实践这么多年下来总结出几条实在的经验。第一条跟前面提到的有关——别直接跑python setup.py install。这个命令会把包直接装进当前Python环境的site-packages但没法记录到这个环境里。过段时间换台机器或者换个Python版本重装的时候可能会漏掉依赖。后来社区普遍改用pip install .或者pip install -e .pip会处理好依赖和记录回退也方便。第二条是把项目配置信息抽出来。setup.py里的metadata太多不是好事容易跟其他业务逻辑混在一起。现在更推荐用setup.cfg或者pyproject.toml来存配置[build-system] requires [setuptools42] build-backend setuptools.build_meta [project] name myutils version 1.0.0 description 一个常用工具集合 dependencies [ requests2.20.0, click, ]这样做的好处是干净——setup.py只需要保留构建相关的逻辑比如编译C扩展的那些特殊操作。一般项目甚至可以只放一行from setuptools import setup; setup()。第三条是关于版本号的。版本号写在setup.py里但项目代码里可能也要用。重复写两份容易不一致。有人会从setup.py里读版本但更干净的方式是用importlib.metadatafromimportlib.metadataimportversion __version__version(myutils)或者在包的__init__.py里写版本然后setup.py从那里import进来。第四条是依赖管理。install_requires要只写必须的依赖千万别把测试工具、代码格式化工具写进去。那些应该放在extras_require或者dev-requirements.txt里。另外版本号的范围要克制——写得太死会给用户带来麻烦写得太多又容易装错版本。和同类技术对比近几年setup.py的地位有点微妙。PyPAPython Packaging Authority给出的官方推荐是优先使用pyproject.toml其次是setup.cfg最后才是setup.py。pyproject.toml的好处在于它把构建系统也声明化了。过去装个包得先装setuptools然后跑setup.py。pyproject.toml里直接告诉pip要用什么工具来构建省得猜来猜去。而且格式是标准化的TOML解析起来稳定不容易出错。不过pyproject.toml目前还不是万能的。有些特殊需求——比如编译过程中需要生成一些文件、需要根据环境变量条件编译——还是得靠setup.py。pyproject.toml的灵活性有限因为它只接受静态声明。还有一个叫flit的工具走的是另一个方向。它推崇极简主义配置文件特别精简如果想快速发布一个纯Python包flit用着挺顺手。但是对于有C扩展、有复杂依赖的项目flit就显得力不从心了。说到poetry这个工具把依赖管理和打包发布整合在了一起。它也用pyproject.toml但加了专门的锁定文件。跟传统方式对比poetry更像是一个全生命周期的项目管理工具而不仅仅是打包工具。不过生态上还没完全统一有些持续集成工具对poetry的支持还不够好。从实际使用场景来看选择哪个工具得看项目复杂度。小工具、个人项目用flit就很舒服。中型项目团队统一的poetry能把整个流程串起来。如果涉及底层C扩展、自动化生成代码、跨语言编译那还是老老实实写setup.py配合pyproject.toml做静态声明。说到底setup.py像是一把老式扳手没那么好看但什么都能拆。pyproject.toml和poetry则是专用工具针对性更强。选哪个不选哪个得把手头的活看清楚再说。