从源码到分发:详解Python setuptools打包实战与最佳实践
1. 为什么需要setuptools打包第一次写Python脚本的时候你可能直接把.py文件发给同事就能运行。但当项目逐渐复杂依赖增多你会发现这种方式越来越不靠谱。我遇到过最尴尬的情况是脚本在自己电脑跑得好好的同事却报错说缺少某个第三方库。更糟的是这个库还依赖特定版本的其他包。setuptools就是解决这些痛点的瑞士军刀。它能帮你自动处理依赖关系不用再手动写requirements.txt然后pip install -r requirements.txt标准化安装流程用户只需要pip install your-package就能搞定所有事情支持多种分发格式既可以生成源码包(tar.gz)也能生成预编译的wheel文件开发模式支持pip install -e .让你在开发时修改代码立即生效举个例子假设你写了个图像处理工具包依赖OpenCV和Pillow。用setuptools打包后用户安装时这些依赖会自动被处理完全不用操心版本冲突问题。2. 从零开始编写setup.py2.1 最小化setup.py配置先来看个最简单的setup.py示例from setuptools import setup, find_packages setup( namemy_image_tools, version0.1.0, packagesfind_packages(), )这个配置已经能工作了但实际项目中我们还需要更多信息。下面是我在一个真实项目中使用的配置模板from setuptools import setup, find_packages with open(README.md, r, encodingutf-8) as fh: long_description fh.read() setup( namemy_awesome_tool, version0.1.0, authorYour Name, author_emailyour.emailexample.com, descriptionA short description of your project, long_descriptionlong_description, long_description_content_typetext/markdown, urlhttps://github.com/yourusername/yourproject, packagesfind_packages(exclude[tests*]), classifiers[ Programming Language :: Python :: 3, License :: OSI Approved :: MIT License, Operating System :: OS Independent, ], python_requires3.6, install_requires[ numpy1.18.0, opencv-python4.2.0, ], )关键参数说明name包名pip install时用的就是这个version遵循语义化版本规范(MAJOR.MINOR.PATCH)packages使用find_packages()自动发现所有包install_requires声明依赖项pip会自动安装2.2 处理非Python文件如果你的包需要包含数据文件比如预训练模型、配置文件等需要额外配置setup( # ...其他参数... package_data{ my_package: [*.json, models/*.h5] }, include_package_dataTrue, )我曾经踩过一个坑忘记加include_package_dataTrue结果打包时数据文件全丢了用户安装后各种报错找不到文件。3. 打包与分发实战3.1 生成源码包和wheel包执行这两个命令就能生成所有分发文件# 生成源码包(.tar.gz) python setup.py sdist # 生成wheel包(.whl) python setup.py bdist_wheel生成的文件会放在dist目录下。我强烈建议同时生成两种格式源码包兼容性最好但安装时需要编译wheel包安装最快但需要匹配Python版本和平台3.2 本地测试安装在正式发布前一定要先本地测试安装# 在虚拟环境中测试 python -m venv test_env source test_env/bin/activate # Linux/Mac test_env\Scripts\activate # Windows pip install dist/my_package-0.1.0-py3-none-any.whl测试时要检查是否能正常import所有命令行工具是否可用数据文件是否在正确位置3.3 发布到PyPI发布到PyPI其实很简单# 先安装twine pip install twine # 上传 twine upload dist/*不过在上传前建议先上传到测试PyPItwine upload --repository testpypi dist/*我有个惨痛教训第一次发布时版本号写成了0.0.1后来想更新发现不能覆盖只能递增版本号。4. 高级技巧与最佳实践4.1 可编辑安装模式开发时用这个命令安装pip install -e .这会在你的Python环境中创建一个链接指向源码目录修改代码后立即生效不用重新安装。我在开发一个机器学习框架时这个功能节省了大量时间。4.2 入口点(entry_points)想让你的包提供命令行工具这样配置setup( # ...其他参数... entry_points{ console_scripts: [ my-toolmy_package.cli:main, ], }, )安装后用户就能直接运行my-tool命令了。我曾经用这个特性把一个复杂的Python脚本变成了团队共享的工具。4.3 多环境依赖管理如果你的包在不同环境需要不同依赖setup( # ...其他参数... extras_require{ dev: [pytest6.0, black], gpu: [cupy-cuda11x], }, )用户可以用pip install my-package[gpu]来安装GPU版本的依赖。5. 常见问题与解决方案5.1 版本冲突问题依赖冲突是最常见的问题。我的经验是尽量放宽版本限制比如numpy1.18.0而不是numpy1.20.0用pip check命令检查冲突考虑使用依赖隔离工具如poetry或pipenv5.2 打包时漏掉文件如果发现打包后缺少文件检查MANIFEST.in文件是否配置正确确认include_package_dataTrue查看package_data参数是否包含所有需要的文件类型5.3 跨平台问题特别是涉及C扩展时要注意为不同平台准备不同的wheel使用环境标记指定平台特定依赖install_requires[ pywin32 1.0; sys_platform win32, ]记得第一次打包一个包含C扩展的项目时Windows用户各种报错后来才发现需要单独编译Windows版的wheel。