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.py
2.1 最小化setup.py配置
先来看个最简单的setup.py示例:
from setuptools import setup, find_packages setup( name="my_image_tools", version="0.1.0", packages=find_packages(), )这个配置已经能工作了,但实际项目中我们还需要更多信息。下面是我在一个真实项目中使用的配置模板:
from setuptools import setup, find_packages with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() setup( name="my_awesome_tool", version="0.1.0", author="Your Name", author_email="your.email@example.com", description="A short description of your project", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/yourusername/yourproject", packages=find_packages(exclude=["tests*"]), classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], python_requires=">=3.6", install_requires=[ "numpy>=1.18.0", "opencv-python>=4.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_data=True, )我曾经踩过一个坑:忘记加include_package_data=True,结果打包时数据文件全丢了,用户安装后各种报错找不到文件。
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/*不过在上传前,建议先上传到测试PyPI:
twine upload --repository testpypi dist/*我有个惨痛教训:第一次发布时版本号写成了0.0.1,后来想更新发现不能覆盖,只能递增版本号。
4. 高级技巧与最佳实践
4.1 可编辑安装模式
开发时用这个命令安装:
pip install -e .这会在你的Python环境中创建一个链接指向源码目录,修改代码后立即生效,不用重新安装。我在开发一个机器学习框架时,这个功能节省了大量时间。
4.2 入口点(entry_points)
想让你的包提供命令行工具?这样配置:
setup( # ...其他参数... entry_points={ "console_scripts": [ "my-tool=my_package.cli:main", ], }, )安装后用户就能直接运行my-tool命令了。我曾经用这个特性把一个复杂的Python脚本变成了团队共享的工具。
4.3 多环境依赖管理
如果你的包在不同环境需要不同依赖:
setup( # ...其他参数... extras_require={ "dev": ["pytest>=6.0", "black"], "gpu": ["cupy-cuda11x"], }, )用户可以用pip install my-package[gpu]来安装GPU版本的依赖。
5. 常见问题与解决方案
5.1 版本冲突问题
依赖冲突是最常见的问题。我的经验是:
- 尽量放宽版本限制(比如
numpy>=1.18.0而不是numpy==1.20.0) - 用
pip check命令检查冲突 - 考虑使用依赖隔离工具如poetry或pipenv
5.2 打包时漏掉文件
如果发现打包后缺少文件:
- 检查
MANIFEST.in文件是否配置正确 - 确认
include_package_data=True - 查看
package_data参数是否包含所有需要的文件类型
5.3 跨平台问题
特别是涉及C扩展时,要注意:
- 为不同平台准备不同的wheel
- 使用环境标记指定平台特定依赖:
install_requires=[ "pywin32 >= 1.0; sys_platform == 'win32'", ]
记得第一次打包一个包含C扩展的项目时,Windows用户各种报错,后来才发现需要单独编译Windows版的wheel。