Setuptools实战:setup()函数详解与Python打包完全指南
前言:setuptools 凭什么统治 Python 打包二十年?
打开 GitHub 上任意一个 Python 项目,你大概率会看到一个 setup.py,里面调用了 setuptools.setup()。二十年来,这几乎是 Python 打包的”唯一标准”。
即使2026年的今天,PyPA 力推 pyproject.toml,setuptools 仍是构建 C 扩展的唯一成熟选择,numpy、scipy、pandas、cryptography 等核心库全部依赖它。
本文以 setuptools 官方 Quickstart(v82.0)为基础,先讲透传统的 setup() 函数,再过渡到现代的声明式配置,让你读懂任何 setuptools 项目。
一、快速入门:5 分钟写出第一个 setup.py
1.1 最小项目结构
1 | my-package/ |
1.2 第一个 setup()
1 | # setup.py |
1.3 构建和安装
1 | # 安装构建工具 |
💡 提示: 不需要手动安装 setuptools,
build工具会从pyproject.toml的[build-system]中自动下载。
1.4 配一个 pyproject.toml
即使你用 setup.py,也建议加一个最小 pyproject.toml 声明构建系统:
1 | [build-system] |
有了这个文件,pip install . 就知道先装 setuptools,再执行构建。
二、setup() 函数完全参数手册
setup() 是 setuptools 的核心,你给他的每一个参数,都定义了你这个”包”的完整面貌。以下是按使用频率排序的完整参数说明。
2.1 元数据参数
1 | from setuptools import setup |
2.2 包发现参数
1 | from setuptools import setup, find_packages |
2.3 依赖参数
1 | setup( |
2.4 入口点参数
1 | setup( |
1 | # 对应的 my_package/cli.py |
安装后直接运行:mycmd Alice
2.5 数据文件参数
1 | setup( |
2.6 C/C++ 扩展参数
这是 setuptools 不可替代的核心能力:
1 | from setuptools import setup, Extension |
1 | // src/_math.c |
1 | pip install . |
2.7 其他重要参数
1 | setup( |
三、三种配置方式全面对比
setuptools 支持三种配置方式,从旧到新:
| 方式 | 文件 | 优缺点 | 现状 |
|---|---|---|---|
setup.py |
Python 代码 | 灵活,但本质是”可执行脚本” | 存量项目主流,新项目不推荐 |
setup.cfg |
INI 格式 | 声明式,比 setup.py 安全 | 过渡方案,逐步淘汰 |
pyproject.toml |
TOML 格式 | 声明式 + 标准化(PEP 621) | PyPA 力推,未来方向 |
3.1 三种方式的相同配置
setup.py 方式:
1 | from setuptools import setup, find_packages |
setup.cfg 方式(声明式,免去代码执行风险):
1 | [metadata] |
1 | # setup.py 变成极简 stub |
pyproject.toml 方式(现代化、PEP 621 标准):
1 | [build-system] |
3.2 配置优先级
当同一个项目同时存在 setup.py、setup.cfg、pyproject.toml 时:
1 | pyproject.toml > setup.cfg > setup.py 的硬编码值 |
后面覆盖前面。所以迁移时可以逐步替换。
四、包发现深入:find_packages() 是怎么工作的?
4.1 原理
1 | from setuptools import find_packages |
4.2 src-layout vs flat-layout
1 | # src-layout(推荐) |
为什么推荐 src-layout?
1 | # flat-layout 暗藏陷阱: |
4.3 排查”包发现失败”
1 | # 诊断脚本:看 setuptools 发现了哪些包 |
五、开发模式(Editable Install)
开发中最常用的功能——改代码立刻生效。
1 | # 安装为"可编辑模式" |
5.1 原理
1 | site-packages/ |
5.2 临时文件 .gitignore
1 | # setup.py 产生的临时文件 |
六、MANIFEST.in 与数据文件完整指南
6.1 MANIFEST.in 是什么?
MANIFEST.in 控制 sdist(源码分发包) 中包含哪些非 Python 文件。
⚠️ 重要区分:
package_data控制 wheel 包,MANIFEST.in控制 sdist 包。两者都要配置才能完整覆盖。
6.2 常用指令
1 | # MANIFEST.in |
6.3 完整配置对照
1 | # setup.py |
1 | # MANIFEST.in |
6.4 验证打包内容
1 | # 构建 sdist 并检查内容 |
七、入口点深入:entry_points 的三种用法
7.1 自动生成脚本
1 | setup( |
安装后 setuptools 生成跨平台 wrapper:
1 | # 实际生成的脚本(简化版) |
7.2 插件发现系统
1 | # 插件提供方 |
1 | # 主应用方:加载所有插件 |
7.3 Flask / Click 的入口点模式
1 | # Flask 扩展用这种方式注册自己 |
八、实战案例:一个完整的 CLI 工具项目
8.1 项目结构
1 | weather-cli/ |
8.2 setup.py
1 | """Weather CLI - A command-line weather tool.""" |
8.3 setup.cfg(可选,用于 flake8 等不支持 pyproject.toml 的工具)
1 | [flake8] |
8.4 pyproject.toml
1 | [build-system] |
8.5 MANIFEST.in
1 | graft src/weather_cli/config |
8.6 version.py
1 | # src/weather_cli/__version__.py |
8.7 构建与发布
1 | # 安装开发模式 |
九、迁移路线图:setup.py → pyproject.toml
9.1 三步迁移法
第一步(当前可用): 添加 pyproject.toml,保留 setup.py
1 | # pyproject.toml |
第二步(逐步过渡): 把配置从 setup.py 抽到 setup.cfg,setup.py 只留 setup()
1 | # setup.py — 极简 stub |
第三步(最终目标): 全部迁移到 pyproject.toml,删除 setup.py 和 setup.cfg
1 | [build-system] |
9.2 什么时候必须保留 setup.py?
即使全面迁移到 pyproject.toml,以下场景仍需 setup.py:
| 场景 | 原因 |
|---|---|
| C 扩展的自定义编译逻辑 | ext_modules 需要运行时计算 |
| 动态依赖(写脚本检测系统环境) | pyproject.toml 是静态声明 |
| 老 pip 用户(< v21.1)的 editable install | 必须存在 setup.py |
| Cython 编译 | cythonize() 需要在 setup() 中调用 |
1 | # 必须保留 setup.py 的例子:动态 Cython 编译 |
十、常见问题排错
10.1 “error: package directory ‘X’ does not exist”
1 | # setup.py 中的 package_dir 和实际目录不匹配 |
10.2 “ModuleNotFoundError after pip install”
1 | # 可能包没有被包含进 wheel |
10.3 “version not found” (setuptools-scm)
1 | # setuptools-scm 需要 git 仓库和至少一个 tag |
10.4 Editable install 不生效
1 | # 新版 pip(>=21.1)用 PEP 660 |
10.5 数据文件没被包含
1 | # 确认 include_package_data = True |
十一、什么时候用 setuptools?
| 场景 | 推荐 | 备选 |
|---|---|---|
| 包含 C/C++ 扩展 | setuptools(唯一选择) | 无 |
| 纯 Python 新项目 | hatchling | flit |
| 维护大量存量项目 | setuptools | — |
| 需要 Cython 编译 | setuptools | — |
| 只需最简单的打包 | flit | hatchling |
总结
setup() 核心参数速查
| 参数 | 用途 | 示例 |
|---|---|---|
name |
PyPI 唯一名称 | "my-package" |
version |
语义化版本 | "1.0.0" |
packages |
要打包的模块 | find_packages(where="src") |
package_dir |
源码根目录映射 | {"": "src"} |
install_requires |
运行时依赖 | ["requests>=2.28"] |
extras_require |
可选功能依赖 | {"dev": ["pytest"]} |
entry_points |
CLI 入口 + 插件 | {"console_scripts": [...]} |
package_data |
包内数据文件 | {"mypkg": ["*.html"]} |
include_package_data |
按 MANIFEST.in 打包 | True |
ext_modules |
C 扩展模块 | [Extension(...)] |
python_requires |
Python 版本约束 | ">=3.10" |
三种配置方式建议
- 新项目 →
pyproject.toml为主,必要时保留setup.py做动态逻辑 - 老项目维护 → 保持
setup.py,逐步向setup.cfg或pyproject.toml迁移 - C 扩展项目 →
setup.py+pyproject.toml(声明 build-system)
📋 参考资料: