1. 项目概述为什么我们需要打包成whl文件在Python开发的世界里我们写好的代码最终要交付给别人使用或者部署到生产环境。直接把一堆.py源文件扔过去显然不是个优雅的办法。想象一下你开发了一个超好用的数据处理工具库你的同事想用你得告诉他“兄弟先把我这个utils文件夹拷过去然后看看requirements.txt里这十几个依赖包你装没装哦对了运行前记得把项目根目录加到PYTHONPATH里……” 这体验太糟糕了既容易出错也极不专业。这时候whl文件就该登场了。whl发音同“wheel”是Python的“轮子”它是一种内置的二进制分发格式。你可以把它理解为一个“软件安装包”就像Windows上的.msi或者.exe安装程序。当你把一个Python项目打包成whl文件后用户只需要一条简单的命令pip install your_package.whl就能完成从安装、解决依赖到配置环境的所有步骤。对于库的开发者而言这意味着标准化的分发对于使用者而言这意味着简单可靠的安装体验。更重要的是whl文件是发布到PyPIPython官方的软件仓库的“标准门票”。无论是像numpy、pandas这样的明星项目还是你个人开发的小工具最终都是以whl的形式被全球开发者下载和安装。掌握whl打包是Python开发者从“写脚本”迈向“做项目”、从“个人使用”走向“协作共享”的关键一步。它让你的代码变得可分发、可复用、可管理。2. 打包前的核心准备项目结构与配置解析在动手打包之前一个清晰、标准的项目结构是成功的基石。混乱的目录会让打包工具无所适从也为你后续的维护埋下地雷。2.1 标准项目目录结构一个典型的、适合打包的Python项目目录应该长这样my_awesome_package/ ├── my_awesome_package/ # 主包目录名字应与项目名一致 │ ├── __init__.py # 包的标识文件可以是空文件也可定义__version__等 │ ├── core.py # 核心模块 │ └── utils/ # 子包 │ ├── __init__.py │ └── helpers.py ├── tests/ # 测试目录 │ ├── __init__.py │ └── test_core.py ├── docs/ # 文档目录可选 ├── README.md # 项目说明至关重要 ├── LICENSE # 开源许可证必须要有 ├── pyproject.toml # 现代构建系统配置文件推荐 ├── setup.py # 传统构建配置文件备用 ├── setup.cfg # 静态配置常与setup.py搭配 └── requirements.txt # 开发依赖清单可选这里有几个关键点双层目录结构最外层的my_awesome_package是项目根目录里面同名的my_awesome_package文件夹才是真正的Python包目录。这避免了顶级模块命名冲突是PyPI上项目的标准做法。__init__.py它告诉Python这个目录是一个包。即使它是空的也必须存在。README.md和LICENSE这是项目的门面和法律文件。没有清晰的README别人不知道你的项目是干嘛的没有LICENSE在法律上意味着别人不能使用你的代码。选择一个合适的开源许可证如MIT、Apache 2.0并放入LICENSE文件。2.2 配置文件的选择与详解pyproject.tomlvssetup.py这是打包配置的核心。传统上我们使用setup.py但现代Python打包更推荐使用pyproject.toml。它更清晰、更安全因为setup.py是可执行代码可能存在风险并且是PEP 518标准。pyproject.toml(推荐方式)这是一个TOML格式的静态配置文件。一个最基础的配置如下[build-system] requires [setuptools61.0, wheel] build-backend setuptools.build_meta [project] name my-awesome-package version 0.1.0 authors [ {name Your Name, email youexample.com}, ] description A short description of your awesome package. readme README.md license {text MIT} classifiers [ Programming Language :: Python :: 3, License :: OSI Approved :: MIT License, Operating System :: OS Independent, ] dependencies [ requests2.25.0, numpy1.20.0, ] [project.optional-dependencies] dev [pytest6.0, black22.0] [project.urls] Homepage https://github.com/you/my_awesome_package Bug-Tracker https://github.com/you/my_awesome_package/issues关键字段解析[build-system]: 定义了构建本包需要什么工具。setuptools和wheel是必须的。name: 包在PyPI上的名称只能用小写字母、数字、连字符和下划线。version: 遵循语义化版本规范主版本号.次版本号.修订号。dependencies: 你的包运行时所依赖的其他包。用户安装你的包时pip会自动安装这里列出的包。[project.optional-dependencies]: 可选依赖比如开发(dev)、测试(test)需要的包不会默认安装。setup.py(传统方式仍需了解)如果你的项目非常复杂或者需要动态生成版本号等可能还需要setup.py。一个最小化的示例如下from setuptools import setup, find_packages setup( namemy-awesome-package, version0.1.0, authorYour Name, author_emailyouexample.com, descriptionA short description, long_descriptionopen(README.md).read(), long_description_content_typetext/markdown, packagesfind_packages(), install_requires[ requests2.25.0, numpy1.20.0, ], classifiers[ Programming Language :: Python :: 3, License :: OSI Approved :: MIT License, Operating System :: OS Independent, ], python_requires3.8, )注意find_packages()会自动发现项目中的所有包这对于标准结构非常方便。但如果你有特殊的目录安排可能需要手动指定packages[my_awesome_package]。实操心得对于新项目我强烈建议从pyproject.toml开始。它更简洁也是社区的趋势。只有当你有动态配置需求例如从git tag读取版本号时才考虑使用或配合setup.py。你可以同时拥有pyproject.toml和setup.py构建工具会优先使用pyproject.toml中的配置。3. 核心打包工具链setuptools与wheel实战打包动作的执行依赖于两个核心库setuptools和wheel。setuptools是构建包的基础框架而wheel则是生成.whl文件格式的工具。3.1 构建环境的搭建与依赖安装首先确保你有一个干净的构建环境。最佳实践是使用虚拟环境Virtual Environment这能避免污染系统Python环境也确保依赖的纯净。# 在项目根目录下创建虚拟环境 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 升级pip和安装构建工具 pip install --upgrade pip setuptools wheel现在你的环境中已经有了打包所需的一切。检查一下你的pyproject.toml或setup.py是否已经正确配置。3.2 执行打包命令生成whl与源码包打包命令非常简单。在项目根目录即pyproject.toml或setup.py所在的目录下执行python -m build这个命令是现在推荐的标准方式。它会执行两个步骤构建源码分发版sdist生成一个.tar.gz文件里面包含你的源代码和pyproject.toml等元数据。这是最通用的分发格式。构建轮子分发版wheel生成一个.whl文件这就是我们最终想要的二进制分发包。命令执行成功后你会在项目目录下发现一个新建的dist/文件夹里面就躺着你的成果dist/ ├── my_awesome_package-0.1.0-py3-none-any.whl └── my_awesome_package-0.1.0.tar.gz那个以.whl结尾的文件就是我们的目标。文件名遵循一个标准格式{包名}-{版本号}-{Python标签}-{ABI标签}-{平台标签}.whl。例如py3-none-any表示兼容所有Python 3版本、任何ABI和任何平台这是一个“纯Python”轮子。传统命令了解即可 在pyproject.toml普及之前我们使用setup.py直接构建# 生成源码包 python setup.py sdist # 生成wheel包 python setup.py bdist_wheelpython -m build命令内部也是调用这些逻辑但它更标准化也处理了构建隔离环境因此是当前的首选。3.3 打包结果验证与本地安装测试生成whl文件后千万别急着上传。先在本地进行安装测试这是避免“社死”发布一个有问题的包的关键一步。# 从dist目录安装你刚刚打好的whl包 pip install dist/my_awesome_package-0.1.0-py3-none-any.whl安装过程应该很迅速。安装完成后启动Python解释器尝试导入你的包并调用其功能import my_awesome_package print(my_awesome_package.__version__) from my_awesome_package.core import some_function some_function()如果一切正常恭喜你打包成功了接下来记得卸载这个测试安装的包以免影响后续开发。pip uninstall my_awesome_package重要提示在运行python -m build之前务必确保你的虚拟环境中没有安装你的包本身例如通过pip install -e .进行的可编辑安装。否则构建过程可能会包含一些开发路径的缓存文件导致打包结果不纯。一个干净的虚拟环境是最佳选择。4. 高级配置与深度定制详解基础打包能满足大部分需求但一个成熟的项目往往需要更精细的控制。下面这些高级配置能让你打包的whl文件更加专业和强大。4.1 包含与非包含文件管理MANIFEST.in默认情况下setuptools只会包含它识别出的Python包文件.py文件和一些标准的元数据文件如README.md,pyproject.toml。如果你的项目包含静态数据文件如JSON配置文件、图片、模板、C扩展的源代码.c文件或其它需要被打包进去的资源你就需要MANIFEST.in文件来显式声明。MANIFEST.in文件放在项目根目录它的语法很直观# 包含根目录下的所有.txt和.md文件 include *.txt *.md # 递归包含docs目录下的所有文件 recursive-include docs * # 递归包含包内data文件夹下的所有.json文件 recursive-include my_awesome_package/data *.json # 包含一个特定的文件 include LICENSE # 排除所有临时文件或缓存文件 global-exclude *.pyc __pycache__* global-exclude *.swp .DS_Store工作原理MANIFEST.in控制的是源码分发版sdist中包含哪些文件。而wheel包中包含的文件则由setuptools的package_data和exclude_package_data参数在setup.py中或[tool.setuptools]部分在pyproject.toml中来控制。为了保持一致性通常配置好MANIFEST.in并在setup.py中设置include_package_dataTrue让setuptools自动将MANIFEST.in匹配的文件包含进wheel。在pyproject.toml中可以这样配置[tool.setuptools] include-package-data true # 或者更精细地控制 # package-data {my_awesome_package [data/*.json]}4.2 入口点Entry Points配置创建命令行工具这是让你的包从“库”升级为“工具”的神奇功能。通过入口点你可以将包内的某个函数注册为系统级的命令行命令。例如你的包my_awesome_package里有一个cli.py模块里面有一个main()函数。你想让用户安装后直接在命令行输入my-tool就能运行。在pyproject.toml中配置[project.scripts] my-tool my_awesome_package.cli:main在setup.py中则是setup( # ... 其他参数 ... entry_points{ console_scripts: [ my-toolmy_awesome_package.cli:main, ], }, )格式解释‘命令名 “模块路径:函数名”’。用户执行pip install后pip会在系统的可执行文件目录如~/.local/bin中创建一个名为my-tool的脚本这个脚本会自动调用你指定的函数。踩坑记录确保你的入口点函数所在的模块不要在文件顶层写可执行的代码。因为安装时这个模块会被导入如果顶层有立即执行的代码如print(‘hello’)会在安装时就被执行这通常不是你想要的行为。所有逻辑应该封装在函数内由入口点触发。4.3 依赖管理的进阶技巧依赖声明不仅仅是列出包名那么简单。版本限定requests2.25.0,3.0.0表示依赖2.25.0及以上但低于3.0.0的版本。这能保证API兼容性。环境标记Extras除了[project.optional-dependencies]里定义的组你还可以用它来定义功能特性组。例如[project.optional-dependencies] speed [orjson3.0.0] # 更快的JSON解析 all [my-awesome-package[speed]]用户可以通过pip install my-awesome-package[speed]来安装性能增强的额外依赖。Python版本限定在pyproject.toml中使用python_requires ‘3.8’或在setup.py中设置python_requires参数。这能防止不兼容Python版本的用户安装你的包。4.4 处理包含C扩展的包如果你的包包含用C/C编写的扩展模块为了性能打包会复杂一些。你需要确保在setup.py中使用setuptools.Extension来定义扩展模块。用户系统上需要有对应的C编译器如Windows的Visual C Build Tools Linux的gcc。生成的wheel标签将不再是py3-none-any而是会包含具体的平台和ABI信息如cp39-cp39-win_amd64这意味着这个wheel只适用于Windows 64位、Python 3.9。你需要为每个目标平台单独构建wheel。对于包含C扩展的包强烈建议使用cibuildwheel这类工具在CI/CD中自动化地为多个平台构建wheel。5. 发布到PyPI与版本管理策略本地测试通过的whl文件最终的目的是分享给全世界。PyPI就是Python世界的中央仓库。5.1 使用Twine上传到PyPI首先安装上传工具twinepip install twine然后使用twine上传dist/目录下的所有分发文件# 上传到测试PyPI强烈建议先传这里 twine upload --repository-url https://test.pypi.org/legacy/ dist/* # 测试安装来自TestPyPI的包 pip install --index-url https://test.pypi.org/simple/ my-awesome-package # 确认一切无误后上传到正式的PyPI twine upload dist/*重要安全提示永远不要将密码明文写在命令行或脚本里twine会交互式地提示你输入用户名和密码。使用API令牌Token更安全。在PyPI官网账户设置中生成一个令牌用它来代替密码。上传时用户名填__token__密码填生成的令牌以pypi-开头。务必先在TestPyPI测试TestPyPI是一个独立的、用于演练的仓库避免你把有问题的版本发布到正式环境。5.2 版本号管理的最佳实践版本号是软件与用户之间的重要契约。遵循语义化版本Semantic Versioning, SemVer规则主版本号MAJOR当你做了不兼容的API修改。次版本号MINOR当你向下兼容地新增了功能。修订号PATCH当你向下兼容地修复了问题。每次发布新包到PyPI版本号都必须递增且不能与已存在的版本重复。我推荐使用bump2version这样的工具来自动化版本号更新它会同时修改pyproject.toml或setup.py以及代码中的__version__变量并帮你打上git tag。5.3 持续集成CI自动化打包发布手动执行打包、测试、上传的流程既繁琐又容易出错。将其集成到GitHub Actions、GitLab CI等持续集成服务中是专业的选择。一个简单的GitHub Actions工作流示例.github/workflows/publish.ymlname: Publish Python Package on: release: types: [published] jobs: build-and-publish: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.x - name: Install dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build package run: python -m build - name: Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: twine upload dist/*这个工作流会在你于GitHub上创建一个新的Release时自动触发完成构建并上传到PyPI。你需要将PyPI的API令牌保存在GitHub仓库的Secrets中名为PYPI_API_TOKEN。6. 常见问题、故障排查与实战心得即使流程清晰打包路上依然会遇到各种“坑”。这里记录了一些典型问题和我的解决经验。6.1 典型错误与解决方案速查表错误现象可能原因解决方案ModuleNotFoundError: No module named ‘my_package’安装后无法导入1. 包目录结构不正确__init__.py缺失或位置不对。2.packages参数未在setup.py中正确设置或pyproject.toml未正确声明。1. 检查是否为标准的双层目录结构确保每个包目录都有__init__.py。2. 使用find_packages()或手动列出所有包。在pyproject.toml中setuptools默认会自动发现包。打包时警告warning: sdist: standard file not found…缺少一些标准文件如LICENSE、README.md等。补全这些文件。即使内容简单一个README.md和LICENSE文件对于正式包是必须的。安装时提示依赖版本冲突在install_requires/dependencies中指定的版本范围与用户环境中已安装的包冲突。放宽版本限制如将2.0.0改为2.0.0,3.0.0或明确声明与哪些主要版本兼容。使用pip check来诊断冲突。whl文件安装成功但命令行工具entry point不生效1. 入口点脚本安装的路径如~/.local/bin不在系统的PATH环境变量中。2. 虚拟环境未激活或安装到了错误的Python环境。1. 检查并添加脚本目录到PATH。2. 确认激活了正确的虚拟环境并使用which my-toolLinux/Mac或where my-toolWindows检查命令路径。打包时包含了大量无关文件如.pyc,__pycache__没有正确使用MANIFEST.in排除或构建环境不干净。1. 在MANIFEST.in中添加global-exclude __pycache__和global-exclude *.pyc。2. 在干净的虚拟环境中进行打包。上传到PyPI时失败提示“File already exists”你尝试上传一个与已有版本号相同的文件。PyPI不允许覆盖版本你必须提升版本号如从0.1.0改为0.1.1后再重新打包上传。6.2 虚拟环境与依赖隔离的教训这是我早期踩过的最大的坑在系统Python环境或一个混乱的虚拟环境中打包。结果打出来的whl文件在我这能运行换台电脑就报错因为里面混入了本地特有的路径或开发依赖。黄金法则永远在一个全新的、只安装了pip、setuptools、wheel和项目运行时依赖的虚拟环境中进行打包。可以使用pip list检查环境是否干净。一个可靠的自动化方式是使用tox或nox工具它们能为你创建临时的、隔离的构建环境。6.3 多平台与Python版本兼容性考量如果你的包是“纯Python”的即不包含C扩展那么一个py3-none-any.whl文件就能通吃所有支持Python 3的平台和架构。这是最省心的。但如果你用了任何与平台相关的特性哪怕只是通过ctypes调用了一个系统库或者包含了C扩展你就需要为每个目标平台Windows, macOS, Linux的不同架构和每个主要的Python版本如cp37, cp38, cp39等分别构建对应的wheel。这就是为什么你在PyPI上看到numpy、pandas等库有几十个不同的whl文件。对于个人或小团队维护多平台构建矩阵是沉重的负担。解决方案是尽可能保持“纯Python”。如果必须用C扩展使用cibuildwheel。它可以轻松地在CI服务如GitHub Actions上配置自动为Windows、macOS、Linux的多个版本构建wheel。提供源码包sdist作为兜底。如果用户平台没有对应的预编译wheelpip会自动回退到下载sdist并在本地编译但这要求用户有编译环境。6.4 测试与质量保障打包后的冒烟测试打包完成不是终点。建立一个简单的“冒烟测试”流程至关重要。我通常会在本地安装刚打好的whl包后运行一个最简短的测试脚本或者直接运行包提供的命令行工具。更好的做法是将这个测试集成到CI流程中在CI里构建wheel在一个新的、干净的虚拟环境中安装它然后运行项目的单元测试。这能确保打包过程没有引入任何破坏性变化。最后关于版本号我再啰嗦一句在0.y.z的初始开发阶段你可以相对自由地增加版本。但一旦你发布了1.0.0就要严肃对待版本号的变化因为它向用户传递了兼容性变化的信号。每次提交前问问自己“这个改动需要升主版本、次版本还是修订号” 养成这个习惯你会成为一个更受信赖的维护者。
Python项目打包成whl文件:从配置到发布的完整指南
1. 项目概述为什么我们需要打包成whl文件在Python开发的世界里我们写好的代码最终要交付给别人使用或者部署到生产环境。直接把一堆.py源文件扔过去显然不是个优雅的办法。想象一下你开发了一个超好用的数据处理工具库你的同事想用你得告诉他“兄弟先把我这个utils文件夹拷过去然后看看requirements.txt里这十几个依赖包你装没装哦对了运行前记得把项目根目录加到PYTHONPATH里……” 这体验太糟糕了既容易出错也极不专业。这时候whl文件就该登场了。whl发音同“wheel”是Python的“轮子”它是一种内置的二进制分发格式。你可以把它理解为一个“软件安装包”就像Windows上的.msi或者.exe安装程序。当你把一个Python项目打包成whl文件后用户只需要一条简单的命令pip install your_package.whl就能完成从安装、解决依赖到配置环境的所有步骤。对于库的开发者而言这意味着标准化的分发对于使用者而言这意味着简单可靠的安装体验。更重要的是whl文件是发布到PyPIPython官方的软件仓库的“标准门票”。无论是像numpy、pandas这样的明星项目还是你个人开发的小工具最终都是以whl的形式被全球开发者下载和安装。掌握whl打包是Python开发者从“写脚本”迈向“做项目”、从“个人使用”走向“协作共享”的关键一步。它让你的代码变得可分发、可复用、可管理。2. 打包前的核心准备项目结构与配置解析在动手打包之前一个清晰、标准的项目结构是成功的基石。混乱的目录会让打包工具无所适从也为你后续的维护埋下地雷。2.1 标准项目目录结构一个典型的、适合打包的Python项目目录应该长这样my_awesome_package/ ├── my_awesome_package/ # 主包目录名字应与项目名一致 │ ├── __init__.py # 包的标识文件可以是空文件也可定义__version__等 │ ├── core.py # 核心模块 │ └── utils/ # 子包 │ ├── __init__.py │ └── helpers.py ├── tests/ # 测试目录 │ ├── __init__.py │ └── test_core.py ├── docs/ # 文档目录可选 ├── README.md # 项目说明至关重要 ├── LICENSE # 开源许可证必须要有 ├── pyproject.toml # 现代构建系统配置文件推荐 ├── setup.py # 传统构建配置文件备用 ├── setup.cfg # 静态配置常与setup.py搭配 └── requirements.txt # 开发依赖清单可选这里有几个关键点双层目录结构最外层的my_awesome_package是项目根目录里面同名的my_awesome_package文件夹才是真正的Python包目录。这避免了顶级模块命名冲突是PyPI上项目的标准做法。__init__.py它告诉Python这个目录是一个包。即使它是空的也必须存在。README.md和LICENSE这是项目的门面和法律文件。没有清晰的README别人不知道你的项目是干嘛的没有LICENSE在法律上意味着别人不能使用你的代码。选择一个合适的开源许可证如MIT、Apache 2.0并放入LICENSE文件。2.2 配置文件的选择与详解pyproject.tomlvssetup.py这是打包配置的核心。传统上我们使用setup.py但现代Python打包更推荐使用pyproject.toml。它更清晰、更安全因为setup.py是可执行代码可能存在风险并且是PEP 518标准。pyproject.toml(推荐方式)这是一个TOML格式的静态配置文件。一个最基础的配置如下[build-system] requires [setuptools61.0, wheel] build-backend setuptools.build_meta [project] name my-awesome-package version 0.1.0 authors [ {name Your Name, email youexample.com}, ] description A short description of your awesome package. readme README.md license {text MIT} classifiers [ Programming Language :: Python :: 3, License :: OSI Approved :: MIT License, Operating System :: OS Independent, ] dependencies [ requests2.25.0, numpy1.20.0, ] [project.optional-dependencies] dev [pytest6.0, black22.0] [project.urls] Homepage https://github.com/you/my_awesome_package Bug-Tracker https://github.com/you/my_awesome_package/issues关键字段解析[build-system]: 定义了构建本包需要什么工具。setuptools和wheel是必须的。name: 包在PyPI上的名称只能用小写字母、数字、连字符和下划线。version: 遵循语义化版本规范主版本号.次版本号.修订号。dependencies: 你的包运行时所依赖的其他包。用户安装你的包时pip会自动安装这里列出的包。[project.optional-dependencies]: 可选依赖比如开发(dev)、测试(test)需要的包不会默认安装。setup.py(传统方式仍需了解)如果你的项目非常复杂或者需要动态生成版本号等可能还需要setup.py。一个最小化的示例如下from setuptools import setup, find_packages setup( namemy-awesome-package, version0.1.0, authorYour Name, author_emailyouexample.com, descriptionA short description, long_descriptionopen(README.md).read(), long_description_content_typetext/markdown, packagesfind_packages(), install_requires[ requests2.25.0, numpy1.20.0, ], classifiers[ Programming Language :: Python :: 3, License :: OSI Approved :: MIT License, Operating System :: OS Independent, ], python_requires3.8, )注意find_packages()会自动发现项目中的所有包这对于标准结构非常方便。但如果你有特殊的目录安排可能需要手动指定packages[my_awesome_package]。实操心得对于新项目我强烈建议从pyproject.toml开始。它更简洁也是社区的趋势。只有当你有动态配置需求例如从git tag读取版本号时才考虑使用或配合setup.py。你可以同时拥有pyproject.toml和setup.py构建工具会优先使用pyproject.toml中的配置。3. 核心打包工具链setuptools与wheel实战打包动作的执行依赖于两个核心库setuptools和wheel。setuptools是构建包的基础框架而wheel则是生成.whl文件格式的工具。3.1 构建环境的搭建与依赖安装首先确保你有一个干净的构建环境。最佳实践是使用虚拟环境Virtual Environment这能避免污染系统Python环境也确保依赖的纯净。# 在项目根目录下创建虚拟环境 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 升级pip和安装构建工具 pip install --upgrade pip setuptools wheel现在你的环境中已经有了打包所需的一切。检查一下你的pyproject.toml或setup.py是否已经正确配置。3.2 执行打包命令生成whl与源码包打包命令非常简单。在项目根目录即pyproject.toml或setup.py所在的目录下执行python -m build这个命令是现在推荐的标准方式。它会执行两个步骤构建源码分发版sdist生成一个.tar.gz文件里面包含你的源代码和pyproject.toml等元数据。这是最通用的分发格式。构建轮子分发版wheel生成一个.whl文件这就是我们最终想要的二进制分发包。命令执行成功后你会在项目目录下发现一个新建的dist/文件夹里面就躺着你的成果dist/ ├── my_awesome_package-0.1.0-py3-none-any.whl └── my_awesome_package-0.1.0.tar.gz那个以.whl结尾的文件就是我们的目标。文件名遵循一个标准格式{包名}-{版本号}-{Python标签}-{ABI标签}-{平台标签}.whl。例如py3-none-any表示兼容所有Python 3版本、任何ABI和任何平台这是一个“纯Python”轮子。传统命令了解即可 在pyproject.toml普及之前我们使用setup.py直接构建# 生成源码包 python setup.py sdist # 生成wheel包 python setup.py bdist_wheelpython -m build命令内部也是调用这些逻辑但它更标准化也处理了构建隔离环境因此是当前的首选。3.3 打包结果验证与本地安装测试生成whl文件后千万别急着上传。先在本地进行安装测试这是避免“社死”发布一个有问题的包的关键一步。# 从dist目录安装你刚刚打好的whl包 pip install dist/my_awesome_package-0.1.0-py3-none-any.whl安装过程应该很迅速。安装完成后启动Python解释器尝试导入你的包并调用其功能import my_awesome_package print(my_awesome_package.__version__) from my_awesome_package.core import some_function some_function()如果一切正常恭喜你打包成功了接下来记得卸载这个测试安装的包以免影响后续开发。pip uninstall my_awesome_package重要提示在运行python -m build之前务必确保你的虚拟环境中没有安装你的包本身例如通过pip install -e .进行的可编辑安装。否则构建过程可能会包含一些开发路径的缓存文件导致打包结果不纯。一个干净的虚拟环境是最佳选择。4. 高级配置与深度定制详解基础打包能满足大部分需求但一个成熟的项目往往需要更精细的控制。下面这些高级配置能让你打包的whl文件更加专业和强大。4.1 包含与非包含文件管理MANIFEST.in默认情况下setuptools只会包含它识别出的Python包文件.py文件和一些标准的元数据文件如README.md,pyproject.toml。如果你的项目包含静态数据文件如JSON配置文件、图片、模板、C扩展的源代码.c文件或其它需要被打包进去的资源你就需要MANIFEST.in文件来显式声明。MANIFEST.in文件放在项目根目录它的语法很直观# 包含根目录下的所有.txt和.md文件 include *.txt *.md # 递归包含docs目录下的所有文件 recursive-include docs * # 递归包含包内data文件夹下的所有.json文件 recursive-include my_awesome_package/data *.json # 包含一个特定的文件 include LICENSE # 排除所有临时文件或缓存文件 global-exclude *.pyc __pycache__* global-exclude *.swp .DS_Store工作原理MANIFEST.in控制的是源码分发版sdist中包含哪些文件。而wheel包中包含的文件则由setuptools的package_data和exclude_package_data参数在setup.py中或[tool.setuptools]部分在pyproject.toml中来控制。为了保持一致性通常配置好MANIFEST.in并在setup.py中设置include_package_dataTrue让setuptools自动将MANIFEST.in匹配的文件包含进wheel。在pyproject.toml中可以这样配置[tool.setuptools] include-package-data true # 或者更精细地控制 # package-data {my_awesome_package [data/*.json]}4.2 入口点Entry Points配置创建命令行工具这是让你的包从“库”升级为“工具”的神奇功能。通过入口点你可以将包内的某个函数注册为系统级的命令行命令。例如你的包my_awesome_package里有一个cli.py模块里面有一个main()函数。你想让用户安装后直接在命令行输入my-tool就能运行。在pyproject.toml中配置[project.scripts] my-tool my_awesome_package.cli:main在setup.py中则是setup( # ... 其他参数 ... entry_points{ console_scripts: [ my-toolmy_awesome_package.cli:main, ], }, )格式解释‘命令名 “模块路径:函数名”’。用户执行pip install后pip会在系统的可执行文件目录如~/.local/bin中创建一个名为my-tool的脚本这个脚本会自动调用你指定的函数。踩坑记录确保你的入口点函数所在的模块不要在文件顶层写可执行的代码。因为安装时这个模块会被导入如果顶层有立即执行的代码如print(‘hello’)会在安装时就被执行这通常不是你想要的行为。所有逻辑应该封装在函数内由入口点触发。4.3 依赖管理的进阶技巧依赖声明不仅仅是列出包名那么简单。版本限定requests2.25.0,3.0.0表示依赖2.25.0及以上但低于3.0.0的版本。这能保证API兼容性。环境标记Extras除了[project.optional-dependencies]里定义的组你还可以用它来定义功能特性组。例如[project.optional-dependencies] speed [orjson3.0.0] # 更快的JSON解析 all [my-awesome-package[speed]]用户可以通过pip install my-awesome-package[speed]来安装性能增强的额外依赖。Python版本限定在pyproject.toml中使用python_requires ‘3.8’或在setup.py中设置python_requires参数。这能防止不兼容Python版本的用户安装你的包。4.4 处理包含C扩展的包如果你的包包含用C/C编写的扩展模块为了性能打包会复杂一些。你需要确保在setup.py中使用setuptools.Extension来定义扩展模块。用户系统上需要有对应的C编译器如Windows的Visual C Build Tools Linux的gcc。生成的wheel标签将不再是py3-none-any而是会包含具体的平台和ABI信息如cp39-cp39-win_amd64这意味着这个wheel只适用于Windows 64位、Python 3.9。你需要为每个目标平台单独构建wheel。对于包含C扩展的包强烈建议使用cibuildwheel这类工具在CI/CD中自动化地为多个平台构建wheel。5. 发布到PyPI与版本管理策略本地测试通过的whl文件最终的目的是分享给全世界。PyPI就是Python世界的中央仓库。5.1 使用Twine上传到PyPI首先安装上传工具twinepip install twine然后使用twine上传dist/目录下的所有分发文件# 上传到测试PyPI强烈建议先传这里 twine upload --repository-url https://test.pypi.org/legacy/ dist/* # 测试安装来自TestPyPI的包 pip install --index-url https://test.pypi.org/simple/ my-awesome-package # 确认一切无误后上传到正式的PyPI twine upload dist/*重要安全提示永远不要将密码明文写在命令行或脚本里twine会交互式地提示你输入用户名和密码。使用API令牌Token更安全。在PyPI官网账户设置中生成一个令牌用它来代替密码。上传时用户名填__token__密码填生成的令牌以pypi-开头。务必先在TestPyPI测试TestPyPI是一个独立的、用于演练的仓库避免你把有问题的版本发布到正式环境。5.2 版本号管理的最佳实践版本号是软件与用户之间的重要契约。遵循语义化版本Semantic Versioning, SemVer规则主版本号MAJOR当你做了不兼容的API修改。次版本号MINOR当你向下兼容地新增了功能。修订号PATCH当你向下兼容地修复了问题。每次发布新包到PyPI版本号都必须递增且不能与已存在的版本重复。我推荐使用bump2version这样的工具来自动化版本号更新它会同时修改pyproject.toml或setup.py以及代码中的__version__变量并帮你打上git tag。5.3 持续集成CI自动化打包发布手动执行打包、测试、上传的流程既繁琐又容易出错。将其集成到GitHub Actions、GitLab CI等持续集成服务中是专业的选择。一个简单的GitHub Actions工作流示例.github/workflows/publish.ymlname: Publish Python Package on: release: types: [published] jobs: build-and-publish: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.x - name: Install dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build package run: python -m build - name: Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: twine upload dist/*这个工作流会在你于GitHub上创建一个新的Release时自动触发完成构建并上传到PyPI。你需要将PyPI的API令牌保存在GitHub仓库的Secrets中名为PYPI_API_TOKEN。6. 常见问题、故障排查与实战心得即使流程清晰打包路上依然会遇到各种“坑”。这里记录了一些典型问题和我的解决经验。6.1 典型错误与解决方案速查表错误现象可能原因解决方案ModuleNotFoundError: No module named ‘my_package’安装后无法导入1. 包目录结构不正确__init__.py缺失或位置不对。2.packages参数未在setup.py中正确设置或pyproject.toml未正确声明。1. 检查是否为标准的双层目录结构确保每个包目录都有__init__.py。2. 使用find_packages()或手动列出所有包。在pyproject.toml中setuptools默认会自动发现包。打包时警告warning: sdist: standard file not found…缺少一些标准文件如LICENSE、README.md等。补全这些文件。即使内容简单一个README.md和LICENSE文件对于正式包是必须的。安装时提示依赖版本冲突在install_requires/dependencies中指定的版本范围与用户环境中已安装的包冲突。放宽版本限制如将2.0.0改为2.0.0,3.0.0或明确声明与哪些主要版本兼容。使用pip check来诊断冲突。whl文件安装成功但命令行工具entry point不生效1. 入口点脚本安装的路径如~/.local/bin不在系统的PATH环境变量中。2. 虚拟环境未激活或安装到了错误的Python环境。1. 检查并添加脚本目录到PATH。2. 确认激活了正确的虚拟环境并使用which my-toolLinux/Mac或where my-toolWindows检查命令路径。打包时包含了大量无关文件如.pyc,__pycache__没有正确使用MANIFEST.in排除或构建环境不干净。1. 在MANIFEST.in中添加global-exclude __pycache__和global-exclude *.pyc。2. 在干净的虚拟环境中进行打包。上传到PyPI时失败提示“File already exists”你尝试上传一个与已有版本号相同的文件。PyPI不允许覆盖版本你必须提升版本号如从0.1.0改为0.1.1后再重新打包上传。6.2 虚拟环境与依赖隔离的教训这是我早期踩过的最大的坑在系统Python环境或一个混乱的虚拟环境中打包。结果打出来的whl文件在我这能运行换台电脑就报错因为里面混入了本地特有的路径或开发依赖。黄金法则永远在一个全新的、只安装了pip、setuptools、wheel和项目运行时依赖的虚拟环境中进行打包。可以使用pip list检查环境是否干净。一个可靠的自动化方式是使用tox或nox工具它们能为你创建临时的、隔离的构建环境。6.3 多平台与Python版本兼容性考量如果你的包是“纯Python”的即不包含C扩展那么一个py3-none-any.whl文件就能通吃所有支持Python 3的平台和架构。这是最省心的。但如果你用了任何与平台相关的特性哪怕只是通过ctypes调用了一个系统库或者包含了C扩展你就需要为每个目标平台Windows, macOS, Linux的不同架构和每个主要的Python版本如cp37, cp38, cp39等分别构建对应的wheel。这就是为什么你在PyPI上看到numpy、pandas等库有几十个不同的whl文件。对于个人或小团队维护多平台构建矩阵是沉重的负担。解决方案是尽可能保持“纯Python”。如果必须用C扩展使用cibuildwheel。它可以轻松地在CI服务如GitHub Actions上配置自动为Windows、macOS、Linux的多个版本构建wheel。提供源码包sdist作为兜底。如果用户平台没有对应的预编译wheelpip会自动回退到下载sdist并在本地编译但这要求用户有编译环境。6.4 测试与质量保障打包后的冒烟测试打包完成不是终点。建立一个简单的“冒烟测试”流程至关重要。我通常会在本地安装刚打好的whl包后运行一个最简短的测试脚本或者直接运行包提供的命令行工具。更好的做法是将这个测试集成到CI流程中在CI里构建wheel在一个新的、干净的虚拟环境中安装它然后运行项目的单元测试。这能确保打包过程没有引入任何破坏性变化。最后关于版本号我再啰嗦一句在0.y.z的初始开发阶段你可以相对自由地增加版本。但一旦你发布了1.0.0就要严肃对待版本号的变化因为它向用户传递了兼容性变化的信号。每次提交前问问自己“这个改动需要升主版本、次版本还是修订号” 养成这个习惯你会成为一个更受信赖的维护者。