Python 打包与分发:setuptools、poetry 和 PyInstaller 的用法
大家好,今天我们来深入探讨 Python 的打包与分发,主要聚焦于三个重要的工具:setuptools
、poetry
和 PyInstaller
。理解这些工具对于构建可维护、可分发的 Python 项目至关重要。
一、setuptools:Python 打包的基石
setuptools
是 Python 打包的核心库,它扩展了 Python 的 distutils
,提供了更为强大和灵活的打包机制。 使用 setuptools
,我们可以定义项目元数据、依赖关系,并生成各种格式的分发包。
1.1 核心概念:setup.py
setuptools
的核心在于 setup.py
文件。 这个文件定义了项目的构建和安装过程。 让我们看一个简单的例子:
# setup.py
from setuptools import setup, find_packages
setup(
name='my_package',
version='0.1.0',
packages=find_packages(), # 自动查找项目中的所有包
install_requires=[
'requests',
'numpy',
],
entry_points={
'console_scripts': [
'my_script = my_package.my_module:main',
],
},
author='Your Name',
author_email='[email protected]',
description='A short description of your package',
license='MIT',
url='https://github.com/your_username/my_package',
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
],
)
name
: 包的名称,这是在 PyPI 上注册的名称。version
: 包的版本号,遵循语义化版本控制(Semantic Versioning)。packages
: 列出要包含在包中的所有 Python 包。find_packages()
函数会自动找到项目中的所有包。install_requires
: 指定项目运行时所需的依赖项。 在安装包时,pip
会自动安装这些依赖项。entry_points
: 定义可执行脚本,允许将 Python 函数暴露为命令行工具。 在上面的例子中,my_script
命令会执行my_package.my_module
模块中的main
函数。author
,author_email
,description
,license
,url
: 项目的元数据,用于在 PyPI 上显示项目信息。classifiers
: 用于对包进行分类,方便在 PyPI 上搜索。
1.2 主要命令
setuptools
提供了许多命令来构建和分发包。 这些命令通常通过 python setup.py <command>
的方式执行。
命令 | 描述 |
---|---|
install |
安装包及其依赖项。 |
develop |
以“开发模式”安装包。 这会在 site-packages 目录中创建一个指向项目源代码的链接,方便开发和调试。 |
sdist |
创建源代码分发包(.tar.gz )。 |
bdist_wheel |
创建 wheel 分发包(.whl )。 wheel 是一种二进制包格式,安装速度更快,更可靠。 |
bdist_egg |
创建 egg 分发包(.egg )。 已过时,推荐使用 wheel |
register |
在 PyPI 上注册包。 需要 PyPI 账户和密码 |
upload |
将包上传到 PyPI。 需要 PyPI 账户和密码 |
build_ext --inplace |
在当前目录构建 C 扩展模块。 这对于开发需要编译的模块很有用。 |
1.3 示例:构建和安装包
假设我们有以下项目结构:
my_package/
├── my_package/
│ ├── __init__.py
│ ├── my_module.py
│ └── another_module.py
├── setup.py
my_module.py
的内容如下:
# my_package/my_module.py
def main():
print("Hello from my_package!")
if __name__ == "__main__":
main()
现在,我们可以使用以下命令来构建和安装包:
-
创建源代码分发包:
python setup.py sdist
这会在
dist
目录中创建一个my_package-0.1.0.tar.gz
文件。 -
创建 wheel 分发包:
python setup.py bdist_wheel
这会在
dist
目录中创建一个my_package-0.1.0-py3-none-any.whl
文件。 -
安装包:
pip install . # 从当前目录安装
或者,安装 wheel 文件:
pip install dist/my_package-0.1.0-py3-none-any.whl
-
运行命令行脚本:
安装完成后,就可以在命令行中运行
my_script
命令:my_script
输出:
Hello from my_package!
1.4 MANIFEST.in
:包含非 Python 文件
setuptools
默认只包含 Python 文件。 如果项目需要包含其他类型的文件(例如,配置文件、数据文件、静态资源),则需要创建一个 MANIFEST.in
文件来指定这些文件。
例如,如果想包含 data
目录中的所有文件,可以在 MANIFEST.in
中添加以下行:
include data/*
然后,需要在 setup.py
中添加 include_package_data=True
:
# setup.py
from setuptools import setup, find_packages
setup(
name='my_package',
version='0.1.0',
packages=find_packages(),
install_requires=[
'requests',
'numpy',
],
include_package_data=True, # 包含 MANIFEST.in 中指定的文件
# ...
)
二、Poetry:依赖管理与打包的现代解决方案
Poetry
是一个现代的 Python 依赖管理和打包工具。 它旨在简化依赖管理,提供一致且可重复的构建过程。 Poetry
解决了 setuptools
在依赖管理方面的一些痛点,例如版本冲突和环境隔离。
2.1 核心概念:pyproject.toml
Poetry
使用 pyproject.toml
文件来管理项目元数据、依赖关系和构建配置。 这是一个 TOML 格式的文件,易于阅读和编辑。
# pyproject.toml
[tool.poetry]
name = "my_package"
version = "0.1.0"
description = "A short description of my package"
authors = ["Your Name <[email protected]>"]
license = "MIT"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.6"
requests = "^2.25.1"
numpy = "^1.20.0"
[tool.poetry.dev-dependencies]
pytest = "^6.2.4"
flake8 = "^3.9.2"
[tool.poetry.scripts]
my_script = "my_package.my_module:main"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
: 定义项目的元数据,例如名称、版本、描述、作者和许可证。[tool.poetry.dependencies]
: 指定项目运行时所需的依赖项。^
符号表示兼容的版本范围。 例如,requests = "^2.25.1"
表示允许安装2.25.1
及更高版本的requests
,但不包括3.0.0
及更高版本。[tool.poetry.dev-dependencies]
: 指定开发时所需的依赖项,例如测试框架和代码检查工具。[tool.poetry.scripts]
: 定义可执行脚本,与setuptools
的entry_points
类似。[build-system]
: 指定用于构建项目的构建系统。Poetry
使用poetry-core
作为构建后端。
2.2 主要命令
Poetry
提供了丰富的命令来管理项目依赖关系、构建和发布包。
命令 | 描述 |
---|---|
install |
安装项目依赖项。 Poetry 会创建一个虚拟环境,并将所有依赖项安装到该环境中。 |
add |
向 pyproject.toml 文件添加新的依赖项。 |
remove |
从 pyproject.toml 文件中移除依赖项。 |
update |
更新项目依赖项到最新版本。 |
build |
构建包。 Poetry 会创建源代码分发包(.tar.gz )和 wheel 分发包(.whl )。 |
publish |
将包发布到 PyPI。 需要 PyPI 账户和密码 |
run |
在项目的虚拟环境中运行命令。 例如,poetry run python my_script.py 。 |
shell |
激活项目的虚拟环境。 |
show |
显示已安装的依赖项。 |
lock |
创建 poetry.lock 文件,记录项目的确切依赖项版本。 这可以确保项目在不同环境中具有一致的依赖关系。 |
2.3 示例:使用 Poetry 构建和发布包
-
创建新项目:
poetry new my_package cd my_package
这会创建一个包含
pyproject.toml
文件和基本项目结构的目录。 -
添加依赖项:
poetry add requests poetry add --dev pytest flake8
这会将
requests
添加到运行时依赖项,并将pytest
和flake8
添加到开发时依赖项。 -
安装依赖项:
poetry install
这会创建一个虚拟环境并将所有依赖项安装到该环境中。
-
构建包:
poetry build
这会在
dist
目录中创建源代码分发包和 wheel 分发包。 -
发布包:
poetry publish
这会将包发布到 PyPI。 需要配置 PyPI 凭据。 可以使用
poetry config pypi-token.pypi <YOUR_PYPI_TOKEN>
命令配置 token。
2.4 poetry.lock
:锁定依赖关系
poetry.lock
文件记录了项目中所有依赖项的确切版本。 当运行 poetry install
命令时,Poetry
会首先检查 poetry.lock
文件。 如果该文件存在,Poetry
会安装 poetry.lock
文件中指定的版本,而不是使用 pyproject.toml
文件中指定的版本范围。
这可以确保项目在不同环境中具有一致的依赖关系。 建议将 poetry.lock
文件提交到版本控制系统。
三、PyInstaller:将 Python 应用打包成独立可执行文件
PyInstaller
是一个强大的工具,可以将 Python 应用打包成独立的可执行文件,无需安装 Python 解释器或任何依赖项即可运行。 这对于分发桌面应用程序非常有用。
3.1 核心概念:单文件模式 vs. 目录模式
PyInstaller
提供了两种主要的打包模式:
- 单文件模式 (
--onefile
): 将所有依赖项和 Python 代码打包成一个单独的可执行文件。 这使得分发非常方便,但启动速度可能会稍慢。 - 目录模式 (
--onedir
): 将所有依赖项和 Python 代码打包到一个目录中。 可执行文件位于该目录中。 启动速度更快,但需要分发整个目录。
3.2 基本用法
pyinstaller <your_script.py>
这会将 your_script.py
打包成可执行文件,默认使用目录模式。
3.3 常用选项
选项 | 描述 |
---|---|
--onefile |
使用单文件模式。 |
--onedir |
使用目录模式(默认)。 |
--name <name> |
指定可执行文件的名称。 |
--icon <icon> |
指定可执行文件的图标。 |
--noconsole |
隐藏控制台窗口。 适用于 GUI 应用程序。 |
--windowed |
与 --noconsole 相同,隐藏控制台窗口。 |
--add-data <src:dest> |
添加非 Python 文件到打包后的程序。 src 是源文件或目录,dest 是目标位置(相对于可执行文件)。 |
--add-binary <src:dest> |
添加二进制文件到打包后的程序,例如 DLL 文件。 src 是源文件或目录,dest 是目标位置(相对于可执行文件)。 |
--hidden-import <module> |
强制包含某个模块。 当 PyInstaller 无法自动检测到某个模块时,可以使用此选项。 |
--exclude-module <module> |
排除某个模块。 |
--upx-dir <path> |
指定 UPX 可执行文件的路径。 UPX 可以压缩可执行文件,减小文件大小。 需要单独安装 UPX。 |
--runtime-hook <file> |
指定运行时 hook 脚本。 可以在程序启动时执行自定义代码。 |
--splash <image> |
指定启动画面图像。 |
3.4 示例:打包一个简单的 GUI 应用
假设我们有一个使用 tkinter
编写的 GUI 应用 my_gui.py
:
# my_gui.py
import tkinter as tk
def main():
root = tk.Tk()
root.title("My GUI App")
label = tk.Label(root, text="Hello, Tkinter!")
label.pack()
root.mainloop()
if __name__ == "__main__":
main()
我们可以使用以下命令将其打包成一个没有控制台窗口的单文件可执行文件:
pyinstaller --onefile --noconsole --name my_gui my_gui.py
这会在 dist
目录中创建一个 my_gui.exe
文件。 双击该文件即可运行 GUI 应用。
3.5 处理依赖关系和数据文件
PyInstaller
能够自动检测大多数依赖项。 但是,有时可能需要手动指定一些依赖项或数据文件。
-
--hidden-import
: 如果PyInstaller
无法自动检测到某个模块,可以使用--hidden-import
选项强制包含该模块。 例如:pyinstaller --onefile --hidden-import my_module my_script.py
-
--add-data
: 可以使用--add-data
选项添加非 Python 文件到打包后的程序。 例如,如果需要包含一个名为config.txt
的配置文件:pyinstaller --onefile --add-data "config.txt:." my_script.py
这会将
config.txt
文件复制到可执行文件所在的目录中。 在 Python 代码中,可以使用os.path.dirname(sys.executable)
获取可执行文件所在的目录,然后使用os.path.join()
构造config.txt
的完整路径。# my_script.py import os import sys def get_config_path(): if hasattr(sys, '_MEIPASS'): # Running in a PyInstaller bundle return os.path.join(sys._MEIPASS, "config.txt") else: # Running in a normal Python environment return "config.txt" config_path = get_config_path() print(f"Config path: {config_path}")
-
--add-binary
: 与--add-data
类似,但用于添加二进制文件。
3.6 使用 .spec
文件进行高级配置
对于复杂的项目,可以使用 .spec
文件进行更高级的配置。 .spec
文件是一个 Python 脚本,用于定义 PyInstaller
的构建过程。
可以使用以下命令生成 .spec
文件:
pyi-makespec <your_script.py>
然后,可以编辑 .spec
文件以自定义构建过程。 例如,可以修改 datas
列表以添加数据文件:
# my_script.spec
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['my_script.py'],
pathex=[],
binaries=[],
datas=[('config.txt', '.')], # 添加 config.txt 文件
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='my_script',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None )
修改 .spec
文件后,可以使用以下命令构建可执行文件:
pyinstaller my_script.spec
四、选择合适的工具
工具 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
setuptools |
Python 打包的基础,广泛支持,功能强大。 | 依赖管理较为复杂,版本冲突问题需要手动解决。 | 简单的 Python 包,不需要复杂的依赖管理。 |
Poetry |
现代化的依赖管理和打包工具,简化依赖管理,提供一致的构建过程。 | 学习曲线稍陡峭,对于非常老的项目迁移成本较高。 | 需要管理复杂依赖关系的 Python 包,注重构建过程的可重复性。 |
PyInstaller |
将 Python 应用打包成独立可执行文件,无需安装 Python 解释器或任何依赖项即可运行。 | 打包后的文件较大,启动速度可能较慢,对于复杂的应用需要手动配置依赖关系。 | 需要分发桌面应用程序,无需用户安装 Python 环境。 |
总结:工具选择与应用场景
掌握 setuptools
、poetry
和 PyInstaller
是 Python 开发者的重要技能。 setuptools
是打包的基础,poetry
简化了依赖管理,而 PyInstaller
可以将 Python 应用打包成独立的可执行文件。 根据项目的需求选择合适的工具,可以提高开发效率,并更好地分发 Python 应用。