目录
核心概念与决策 第一部分:构建可分发的库包 标准化的项目结构 现代项目配置核心:pyproject.toml 传统工具:setuptools 实战案例:命令行计算器 第二部分:现代包管理工具详解 Poetry:新一代包管理利器 PDM:标准驱动型包管理器 Poetry vs PDM 对比与选择 uv:极速包管理器(2025 年首选) Hatch:PyPA 官方项目管理工具 第三部分:生成可独立运行的应用程序 PyInstaller:最通用的应用打包器 Nuitka:追求性能的编译器 跨平台打包指南 实战案例:GUI应用打包 第四部分:Docker 容器化部署 第五部分:Conda 打包 第六部分:CI/CD 自动化发布 常见问题 总结
1. 概念与选型
Python 打包是将你的代码及其所有依赖项,整合成可在目标环境中轻松运行的分发格式的过程。
Python程序打包的两大流派:

几种打包格式对比
sdist.tar.gz) | ||||
wheel.whl) | ||||
可执行文件.exe/.app) | ||||
| Docker 镜像 | ||||
Conda 包.tar.bz2) |
选型流程
这个流程图将贯穿全文,帮助你在不同场景下做出正确的选择:

1. 构建可分发的库包
1.1 标准化的项目结构
一个清晰的项目结构是所有事情的开端。推荐使用 src 布局,它能避免在开发过程中意外导入未安装的包。
my_awesome_project/├── src/ # 源代码根目录│ └── mypackage/ # 你的包名│ ├── __init__.py # 包的标志文件│ ├── core.py # 核心逻辑│ ├── utils.py # 工具函数│ ├── cli.py # 命令行接口│ └── data/ # 包内数据文件│ └── config.json├── tests/ # 测试代码│ ├── __init__.py│ ├── test_core.py│ ├── test_utils.py│ └── conftest.py├── docs/ # 文档│ ├── index.md│ └── api.md├── .github/ # GitHub 配置│ └── workflows/│ ├── ci.yml│ └── release.yml├── pyproject.toml # 【核心】现代项目配置├── README.md├── LICENSE└── .gitignore为什么使用 src 布局?
# 没有 src 布局时,可能会意外导入本地代码而不是已安装的包import mypackage # 这导入的是当前目录,而不是 site-packages 中的版本# 使用 src 布局后,必须 pip install -e . 才能导入# 这确保了开发环境和用户安装环境的行为一致1.2 现代项目配置核心:pyproject.toml
pyproject.toml 是所有现代 Python 打包工具的主要配置文件。它取代了传统的 setup.py、setup.cfg、requirements.txt 等多个文件。
完整配置示例
[build-system]requires = ["setuptools>=61.0", "wheel"]build-backend = "setuptools.build_meta"[project]name = "my-awesome-package"version = "0.1.0"description = "一个演示用的 Python 包"authors = [ {name = "Your Name", email = "you@example.com"}]maintainers = [ {name = "Maintainer Name", email = "maintainer@example.com"}]license = "MIT"readme = "README.md"requires-python = ">=3.8"keywords = ["example", "packaging", "tutorial"]classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13",]# 核心依赖dependencies = [ "requests>=2.28.0", "click>=8.0.0,<9.0.0", "pydantic>=2.0.0",]# 可选依赖(用户可以用 pip install mypackage[gui] 安装)[project.optional-dependencies]dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", "black>=23.0.0", "ruff>=0.1.0", "mypy>=1.0.0",]gui = [ "PyQt5>=5.15.0", "matplotlib>=3.5.0",]all = [ "mypackage[dev,gui]",]# 命令行入口点[project.scripts]mycli = "mypackage.cli:main"# GUI 入口点[project.gui-scripts]mygui = "mypackage.gui:main"# 项目链接[project.urls]Homepage = "https://github.com/yourname/my-awesome-package"Documentation = "https://my-awesome-package.readthedocs.io"Repository = "https://github.com/yourname/my-awesome-package.git"Issues = "https://github.com/yourname/my-awesome-package/issues"# ========== 以下是 setuptools 特有配置 ==========[tool.setuptools]package-dir = {"" = "src"}packages = {find = {where = ["src"]}}include-package-data = true[tool.setuptools.package-data]mypackage = [ "data/*.json", "data/*.yaml", "templates/*.html",][tool.setuptools.exclude-package-data]mypackage = ["*.pyc", "__pycache__/*"]# ========== 工具配置 ==========[tool.black]line-length = 100target-version = ['py38', 'py39', 'py310', 'py311', 'py312', 'py313'][tool.ruff]line-length = 100select = ["E", "F", "I", "N", "W"][tool.mypy]python_version = "3.8"warn_return_any = truewarn_unused_configs = truestrict = true[tool.pytest.ini_options]testpaths = ["tests"]python_files = ["test_*.py"]python_classes = ["Test*"]python_functions = ["test_*"]addopts = "-v --cov=mypackage --cov-report=html"1.3 传统工具:setuptools
setuptools 是 Python 打包的基石。即使使用 Poetry 或 PDM,它们底层也依赖打包标准(PEP 517/518),而 setuptools 是最经典的构建后端。
构建与发布流程
# 1. 安装构建和发布工具pip install --upgrade build twine# 2. 清理旧的构建产物rm -rf dist/ build/ *.egg-info# 3. 构建包(在项目根目录执行)python -m build# 这会在 dist/ 目录生成两个文件:# - mypackage-0.1.0.tar.gz (sdist - 源码包)# - mypackage-0.1.0-py3-none-any.whl (wheel - 二进制包)# 4. 检查构建产物是否符合 PyPI 规范python -m twine check dist/*# 5. 上传到 TestPyPI 进行测试python -m twine upload --repository testpypi dist/*# 6. 从 TestPyPI 安装测试pip install --index-url https://test.pypi.org/simple/ my-package# 7. 测试通过后,上传到正式 PyPIpython -m twine upload dist/*可编辑模式开发
# 在项目根目录以开发模式安装pip install -e .# 带可选依赖pip install -e ".[dev]"# 这样对源码的任何修改都会立即生效,无需重新安装# 这是日常开发中最常用的安装方式版本管理策略
# 推荐使用语义化版本(Semantic Versioning)# 格式:MAJOR.MINOR.PATCH# 示例版本演进:# 0.1.0 -> 初始开发版本# 0.2.0 -> 新增功能,向后兼容# 0.2.1 -> 修复 bug,向后兼容# 1.0.0 -> 第一个稳定版本# 1.1.0 -> 新增功能,向后兼容# 2.0.0 -> 不兼容的 API 更改# 在 pyproject.toml 中管理版本[project]version = "1.0.0"# 或者使用动态版本(从 VCS 标签读取)# 需要安装 setuptools-scm[build-system]requires = ["setuptools>=61.0", "setuptools-scm>=8.0"][tool.setuptools_scm]write_to = "src/mypackage/_version.py"1.4 案例:命令行计算器
让我们通过一个"科学计算器"项目来实践打包流程。
项目结构
calculator/├── src/│ └── calculator/│ ├── __init__.py│ ├── core.py│ └── cli.py├── tests/│ ├── __init__.py│ ├── test_core.py│ └── conftest.py├── pyproject.toml├── README.md└── LICENSEsrc/calculator/__init__.py - 包的公开接口
"""科学计算器包提供基本的数学运算和命令行接口。"""from .core import Calculator__all__ = ["Calculator"]__version__ = "0.1.0"src/calculator/core.py - 核心功能
"""计算器核心功能模块."""import mathfrom typing import Union, List, OptionalNumber = Union[int, float]classCalculator:"""科学计算器类. Examples: >>> calc = Calculator() >>> calc.add(10, 20) 30 >>> calc.sqrt(16) 4.0 >>> calc.get_history() ['10 + 20 = 30', '√16 = 4.0'] """def__init__(self) -> None:"""初始化计算器.""" self._history: List[str] = []defadd(self, a: Number, b: Number) -> Number:""" 加法运算. Args: a: 第一个加数 b: 第二个加数 Returns: 两数之和 """ result = a + b self._record(f"{a} + {b} = {result}")return resultdefsubtract(self, a: Number, b: Number) -> Number:""" 减法运算. Args: a: 被减数 b: 减数 Returns: 差 """ result = a - b self._record(f"{a} - {b} = {result}")return resultdefmultiply(self, a: Number, b: Number) -> Number:""" 乘法运算. Args: a: 第一个乘数 b: 第二个乘数 Returns: 积 """ result = a * b self._record(f"{a} × {b} = {result}")return resultdefdivide(self, a: Number, b: Number) -> float:""" 除法运算. Args: a: 被除数 b: 除数 Returns: 商 Raises: ZeroDivisionError: 当除数为零时 """if b == 0:raise ZeroDivisionError("除数不能为零") result = a / b self._record(f"{a} ÷ {b} = {result}")return resultdefpower(self, base: Number, exponent: Number) -> float:""" 幂运算. Args: base: 底数 exponent: 指数 Returns: 幂结果 """ result = math.pow(base, exponent) self._record(f"{base} ^ {exponent} = {result}")return resultdefsqrt(self, x: Number) -> float:""" 平方根计算. Args: x: 输入数字 Returns: 平方根 Raises: ValueError: 当输入为负数时 """if x < 0:raise ValueError("不能计算负数的平方根") result = math.sqrt(x) self._record(f"√{x} = {result}")return resultdef_record(self, entry: str) -> None:"""记录计算历史(内部方法).""" self._history.append(entry)# 只保留最近 100 条记录if len(self._history) > 100: self._history.pop(0)defget_history(self) -> List[str]:""" 获取计算历史. Returns: 历史记录列表 """return self._history.copy()defclear_history(self) -> None:"""清除计算历史.""" self._history.clear()src/calculator/cli.py - 命令行接口
"""命令行接口模块."""import sysimport clickfrom .core import Calculator@click.group()@click.version_option(version="0.1.0", prog_name="calculator")defmain():"""科学计算器 - 强大的命令行计算工具."""pass@main.command()@click.argument('a', type=float)@click.argument('b', type=float)defadd(a, b):"""计算 a + b.""" calc = Calculator() result = calc.add(a, b) click.echo(f"结果: {result}")@main.command()@click.argument('a', type=float)@click.argument('b', type=float)defsubtract(a, b):"""计算 a - b.""" calc = Calculator() result = calc.subtract(a, b) click.echo(f"结果: {result}")@main.command()@click.argument('a', type=float)@click.argument('b', type=float)defmultiply(a, b):"""计算 a × b.""" calc = Calculator() result = calc.multiply(a, b) click.echo(f"结果: {result}")@main.command()@click.argument('a', type=float)@click.argument('b', type=float)defdivide(a, b):"""计算 a ÷ b.""" calc = Calculator()try: result = calc.divide(a, b) click.echo(f"结果: {result}")except ZeroDivisionError as e: click.echo(f"错误: {e}", err=True) sys.exit(1)@main.command()@click.argument('base', type=float)@click.argument('exponent', type=float)defpower(base, exponent):"""计算 base ^ exponent.""" calc = Calculator() result = calc.power(base, exponent) click.echo(f"结果: {result}")@main.command()@click.argument('x', type=float)defsqrt(x):"""计算 √x.""" calc = Calculator()try: result = calc.sqrt(x) click.echo(f"结果: {result}")except ValueError as e: click.echo(f"错误: {e}", err=True) sys.exit(1)@main.command()defhistory():"""显示计算历史.""" calc = Calculator() history_list = calc.get_history()ifnot history_list: click.echo("暂无历史记录")else: click.echo("计算历史:")for i, record in enumerate(history_list, 1): click.echo(f" {i}. {record}")if __name__ == '__main__': main()tests/test_core.py - 单元测试
"""计算器核心功能测试."""import pytestfrom calculator.core import CalculatorclassTestCalculator:"""计算器测试类.""" @pytest.fixturedefcalc(self):"""创建计算器实例."""return Calculator()deftest_add(self, calc):"""测试加法."""assert calc.add(10, 20) == 30assert calc.add(-5, 5) == 0assert calc.add(0.1, 0.2) == pytest.approx(0.3)deftest_subtract(self, calc):"""测试减法."""assert calc.subtract(10, 5) == 5assert calc.subtract(0, 5) == -5deftest_multiply(self, calc):"""测试乘法."""assert calc.multiply(3, 4) == 12assert calc.multiply(-2, 3) == -6deftest_divide(self, calc):"""测试除法."""assert calc.divide(10, 2) == 5assert calc.divide(7, 3) == pytest.approx(2.333, rel=1e-3)deftest_divide_by_zero(self, calc):"""测试除以零."""with pytest.raises(ZeroDivisionError, match="除数不能为零"): calc.divide(10, 0)deftest_power(self, calc):"""测试幂运算."""assert calc.power(2, 3) == 8assert calc.power(4, 0.5) == 2.0deftest_sqrt(self, calc):"""测试平方根."""assert calc.sqrt(16) == 4.0assert calc.sqrt(2) == pytest.approx(1.414, rel=1e-3)deftest_sqrt_negative(self, calc):"""测试负数平方根."""with pytest.raises(ValueError, match="不能计算负数的平方根"): calc.sqrt(-1)deftest_history(self, calc):"""测试历史记录.""" calc.add(1, 2) calc.subtract(5, 3) history = calc.get_history()assert len(history) == 2assert"1 + 2 = 3"in history[0]assert"5 - 3 = 2"in history[1]deftest_clear_history(self, calc):"""测试清除历史.""" calc.add(1, 2) calc.clear_history()assert len(calc.get_history()) == 0pyproject.toml - 项目配置
[build-system]requires = ["setuptools>=61.0"]build-backend = "setuptools.build_meta"[project]name = "calculator"version = "0.1.0"description = "一个命令行科学计算器"readme = "README.md"requires-python = ">=3.8"license = "MIT"authors = [ {name = "Your Name", email = "you@example.com"}]dependencies = [ "click>=8.0.0",][project.optional-dependencies]dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", "black>=23.0.0",][project.scripts]calc = "calculator.cli:main"[project.urls]Homepage = "https://github.com/yourname/calculator"Repository = "https://github.com/yourname/calculator.git"[tool.setuptools]package-dir = {"" = "src"}packages = {find = {where = ["src"]}}构建和使用
# 构建包cd calculatorpython -m build# 安装pip install dist/calculator-0.1.0-py3-none-any.whl# 使用 CLI$ calc add 10 20结果: 30$ calc divide 10 0错误: 除数不能为零$ calc sqrt -- -1错误: 不能计算负数的平方根# 注意:传递负数参数时需要使用 `--` 分隔符# 否则 click 会将 -1 误解析为命令行选项# 在 Python 中使用$ python -c "from calculator import Calculator; c=Calculator(); print(c.multiply(6, 7))"422. 现代包管理工具详解
2.1 Poetry:新一代包管理利器
Poetry 是目前最流行的现代 Python 包管理工具。它将依赖管理、虚拟环境管理、打包构建和发布集成于一体。
pipvirtualenv + setup.py + requirements.txt | poetry 命令 |
requirements.txt | poetry.lock |
pyproject.toml |
安装 Poetry
# Linux / macOS - 官方安装脚本curl -sSL https://install.python-poetry.org | python3 -# Windows (PowerShell)(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -# 使用 pip 安装(不推荐,可能导致依赖冲突)pip install poetry# 验证安装poetry --version# 启用 tab 补全(可选)# Bashpoetry completions bash >> ~/.bash_completion# Zshpoetry completions zsh > ~/.zfunc/_poetry# Fishpoetry completions fish > ~/.config/fish/completions/poetry.fish项目初始化
# 创建新项目(会自动生成完整的项目结构)poetry new my-awesome-project# 生成的结构:# my-awesome-project/# ├── my_awesome_project/# │ └── __init__.py# ├── tests/# │ └── __init__.py# ├── pyproject.toml# └── README.md# 在已有项目中初始化(交互式向导)cd existing-projectpoetry init依赖管理核心命令
# ========== 添加依赖 ==========# 添加生产依赖poetry add requests fastapi uvicorn# 添加指定版本poetry add "django>=4.2,<5.0"# 添加开发依赖poetry add --group dev pytest black mypy ruff# 添加多个分组poetry add --group test pytest-cov pytest-mockpoetry add --group docs sphinx# 从 Git 仓库安装poetry add git+https://github.com/org/repo.git# 从 Git 仓库指定分支poetry add git+https://github.com/org/repo.git#main# 从本地路径安装(开发模式)poetry add ../sibling-package --editable# ========== 移除依赖 ==========poetry remove requests# ========== 安装依赖 ==========# 安装所有依赖(包括 dev 组)poetry install# 仅安装生产依赖poetry install --only main# 安装指定组poetry install --with dev,docs# 不安装开发依赖poetry install --without dev# ========== 更新依赖 ==========# 更新所有依赖到最新兼容版本poetry update# 更新指定包poetry update requests fastapi# 只更新补丁版本poetry update --dry-run# ========== 查看依赖 ==========# 列出所有已安装的包poetry show# 查看依赖树poetry show --tree# 查看指定包的详细信息poetry show requests# 查看哪些依赖有可用更新poetry show --outdated完整的 pyproject.toml 示例
[tool.poetry]name = "my-awesome-project"version = "0.1.0"description = "一个使用 Poetry 管理的示例项目"authors = ["Your Name <you@example.com>"]maintainers = ["Maintainer <maintainer@example.com>"]readme = "README.md"license = "MIT"homepage = "https://github.com/yourname/my-awesome-project"repository = "https://github.com/yourname/my-awesome-project"documentation = "https://my-awesome-project.readthedocs.io"keywords = ["example", "poetry", "tutorial"]classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13",]# Python 版本约束[tool.poetry.dependencies]python = "^3.12"requests = "^2.31.0"fastapi = ">=0.109.0,<1.0.0"pydantic = "^2.5.0"click = "^8.1.0"# 可选依赖的另一种写法redis = {version = "^5.0", optional = true}# 开发依赖分组[tool.poetry.group.dev.dependencies]pytest = "^8.0.0"pytest-cov = "^5.0.0"black = "^24.0.0"mypy = "^1.8.0"ruff = "^0.3.0"pre-commit = "^3.6.0"[tool.poetry.group.test.dependencies]httpx = "^0.27.0"factory-boy = "^3.3.0"[tool.poetry.group.docs.dependencies]sphinx = "^7.2.0"sphinx-rtd-theme = "^2.0.0"# 命令行入口点[tool.poetry.scripts]mycli = "my_awesome_project.cli:main"# 构建系统配置[build-system]requires = ["poetry-core"]build-backend = "poetry.core.masonry.api"# ========== 工具配置 ==========[tool.black]line-length = 100target-version = ['py312'][tool.ruff]line-length = 100select = ["E", "F", "I", "N", "W", "UP"][tool.mypy]python_version = "3.12"strict = truewarn_return_any = true[tool.pytest.ini_options]testpaths = ["tests"]addopts = "-v --cov=my_awesome_project --cov-report=html"虚拟环境管理
# Poetry 自动管理虚拟环境# 查看当前虚拟环境信息poetry env info# 列出所有项目虚拟环境poetry env list# 使用指定 Python 版本创建环境poetry env use python3.12poetry env use /usr/bin/python3.13# 在虚拟环境中执行命令poetry run python script.pypoetry run pytestpoetry run black .# 激活虚拟环境(进入子 Shell)poetry shell# 删除虚拟环境poetry env remove python3.12# 配置虚拟环境创建位置(可选)# 在项目目录下创建 .venvpoetry config virtualenvs.in-project true锁文件 (poetry.lock)
# poetry.lock 的作用:# 1. 记录精确的依赖版本和哈希值# 2. 确保团队所有成员和 CI 环境安装的依赖完全一致# 3. 应该提交到版本控制(Git)# 更新锁文件poetry lock# 更新锁文件但不安装poetry lock --no-update# 重新生成锁文件poetry lock --regenerate构建与发布
# 构建源码包和 wheelpoetry build# 仅构建 wheelpoetry build -f wheel# 构建前检查配置poetry check# 发布到 PyPIpoetry publish# 先构建再发布poetry publish --build# 发布到 TestPyPIpoetry config repositories.testpypi https://test.pypi.org/legacy/poetry publish --repository testpypi# 设置 PyPI 凭证poetry config pypi-token.pypi your-api-token2.2 PDM:标准驱动型包管理器
PDM(Python Development Master)是另一个现代包管理器,其最大特色是严格遵循 PEP 标准,支持 PEP 582(本地包目录)。
PDM 的独特优势
PEP 582 支持:可在项目本地 __pypackages__目录安装包,无需虚拟环境PEP 621 原生支持:直接使用 [project]标准格式,而非自定义格式强大的脚本系统:在 pyproject.toml中定义常用命令插件生态:支持通过插件扩展功能
安装 PDM
# 官方安装脚本(推荐)# Linux / macOScurl -sSL https://pdm-project.org/install-pdm.py | python3 -# Windows (PowerShell)(Invoke-WebRequest -Uri https://pdm-project.org/install-pdm.py -UseBasicParsing).Content | python -# 使用 pip 安装pip install pdm# 验证安装pdm --version# 启用补全pdm completion bash > ~/.bash_completion项目初始化
# 创建新项目pdm init my-projectcd my-project# 在已有项目中初始化(交互式)pdm init# 会询问:# - Python 版本# - 项目类型 (library/application)# - 许可证# - 是否使用 src 布局# - 是否添加开发依赖# 生成的结构:# my-project/# ├── src/# │ └── my_project/# │ └── __init__.py# ├── tests/# │ └── __init__.py# ├── pyproject.toml# ├── pdm.lock# └── README.md依赖管理核心命令
# ========== 添加依赖 ==========# 添加生产依赖pdm add requests fastapi uvicorn# 添加到依赖组pdm add -G dev pytest black mypypdm add -G test pytest-covpdm add -G docs sphinx# 添加可选依赖pdm add -G redis redis# 从 Git 安装pdm add git+https://github.com/org/repo.git@main# 从本地路径安装pdm add ../local-package# 以可编辑模式安装pdm add -e ../local-package# ========== 移除依赖 ==========pdm remove requests# ========== 安装依赖 ==========# 安装所有依赖pdm install# 安装指定组pdm install -G devpdm install -G dev -G test# 安装生产依赖和指定组pdm install --prod -G dev# ========== 更新依赖 ==========# 更新所有依赖pdm update# 更新指定包pdm update requests# 检查过时依赖pdm list --outdated# ========== 查看依赖 ==========# 列出所有依赖pdm list# 查看依赖树pdm list --tree# 导出为 requirements.txtpdm export -o requirements.txtpdm export --group dev -o requirements-dev.txtpyproject.toml 示例
[project]name = "my-pdm-project"version = "0.1.0"description = "一个使用 PDM 管理的项目"authors = [ { name = "Your Name", email = "you@example.com" }]license = "MIT"readme = "README.md"requires-python = ">=3.12"dependencies = [ "requests>=2.31.0", "fastapi>=0.109.0", "click>=8.1.0",][project.optional-dependencies]dev = [ "pytest>=8.0.0", "pytest-cov>=5.0.0", "black>=24.0.0", "mypy>=1.8.0", "ruff>=0.3.0",]test = ["httpx>=0.27.0"]docs = [ "sphinx>=7.2.0", "sphinx-rtd-theme>=2.0.0",]all = ["my-pdm-project[dev,test,docs]"][project.scripts]mycli = "my_project.cli:main"[project.urls]Homepage = "https://github.com/yourname/my-pdm-project"Repository = "https://github.com/yourname/my-pdm-project.git"[build-system]requires = ["pdm-backend"]build-backend = "pdm.backend"# ========== PDM 特有配置 ==========[tool.pdm]distribution = true[tool.pdm.dev-dependencies]dev = [ "pytest>=8.0.0", "black>=24.0.0", "mypy>=1.8.0",]test = ["pytest-cov>=5.0.0"]# ========== PDM 脚本系统 ==========[tool.pdm.scripts]# 格式:脚本名 = "命令"lint = "ruff check ."format = "black ."check_format = "black --check ."test = "pytest tests/"test_cov = "pytest --cov=my_project tests/"test_cov_html = "pytest --cov=my_project --cov-report=html tests/"type_check = "mypy src/"all_checks = { composite = ["lint", "check_format", "type_check", "test"] }# 带参数的命令serve = "uvicorn my_project.main:app --reload --port 8000"# 调用 Python 函数hello = "my_project:main()"PDM 脚本系统详解
# PDM 的脚本系统是其最大亮点之一# 运行单个脚本pdm run lintpdm run formatpdm run test# 运行复合脚本(会依次执行多个脚本)pdm run all_checks# 向脚本传递参数pdm run test -k "test_specific" -v# 列出所有可用脚本pdm run --list# 使用预定义钩子脚本[tool.pdm.scripts]# 在安装后自动执行_post_install = "pre-commit install"# 在构建前自动执行_pre_build = "pdm run lint"构建与发布
# 构建包pdm build# 仅构建 wheelpdm build --no-sdist# 发布到 PyPIpdm publish# 发布到 TestPyPIpdm publish --repository testpypi# 设置 PyPI 凭证pdm config pypi.username __token__pdm config pypi.password your-api-token# 构建并发布pdm publish --build2.3 Poetry vs PDM 对比与选择
功能对比表
| 配置格式 | [tool.poetry] | [project] |
| 虚拟环境 | ||
| 依赖解析 | ||
| 锁文件 | poetry.lock | pdm.lock |
| 依赖分组 | group.dev | -G |
| 项目脚本 | ||
| 插件系统 | ||
| 发布周期 | ||
| 社区生态 | ||
| GitHub Stars | ||
| PEP 标准遵循 |
性能对比
# 依赖解析速度(以中型项目为例,约 50 个依赖)# Poetry:首次 ~30s,缓存后 ~5s# PDM:首次 ~20s,缓存后 ~3s# 安装速度# Poetry:~15s# PDM:~10s(增量安装更快)选择建议
选 Poetry 如果你:
希望工具成熟稳定,社区支持好,遇到问题容易找到解决方案 偏好"开箱即用"的体验,不想过多配置 团队已有 Poetry 使用经验
选 PDM 如果你:
追求最新的 PEP 标准(PEP 621 原生支持) 需要强大的脚本系统来管理开发工作流 希望更灵活的依赖分组和更快的依赖解析
两者均可,如果你:
是新项目,两者都能很好满足需求 是团队项目,关键是统一工具,而非具体选哪个
| 标准化 | ||
| 脚本系统 | ||
| 社区生态 | ||
| 学习曲线 | ||
| 适用场景 |
2.4 uv:极速包管理器
uv 是由 Astral(ruff 的开发团队)用 Rust 编写的极速 Python 包管理器。它在 2024-2025 年间迅猛发展,已成为 Python 生态中最受瞩目的工具之一。
uv 统一了 pip、pip-tools、pipx、virtualenv、poetry 等多个工具的功能,同时比 pip 快 10-100 倍:
安装 uv
# 官方安装脚本(推荐)# Windows (PowerShell)powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"# macOS / Linuxcurl -LsSf https://astral.sh/uv/install.sh | sh# 使用 pip 安装pip install uv# 验证安装uv --version项目管理(uv 原生模式)
# ========== 创建项目 ==========# 创建库项目(使用 src 布局)uv init my-project --lib# 创建应用项目uv init my-app# 在已有目录中初始化cd existing-projectuv init# ========== 依赖管理 ==========# 添加依赖uv add requests fastapi uvicorn# 添加指定版本uv add "django>=4.2,<5.0"# 添加开发依赖uv add --dev pytest black mypy ruff# 添加可选依赖uv add --optional redis redisuv add --optional gui PyQt5# 移除依赖uv remove requests# 安装所有依赖(自动创建 .venv)uv sync# 仅安装生产依赖uv sync --no-dev# ========== 运行命令 ==========# 在项目环境中运行命令uv run python script.pyuv run pytestuv run black .# 运行一次性工具(替代 pipx)uvx ruff check .uvx black --check .# ========== 更新依赖 ==========# 更新所有依赖uv lock --upgrade# 更新指定包uv lock --upgrade-package requests# 列出过时依赖uv tree --outdated# ========== 构建与发布 ==========# 构建包uv build# 发布到 PyPIuv publish# 发布到 TestPyPIuv publish --publish-url https://test.pypi.org/legacy/pyproject.toml 示例
[project]name = "my-uv-project"version = "0.1.0"description = "一个使用 uv 管理的项目"readme = "README.md"requires-python = ">=3.12"license = "MIT"authors = [ { name = "Your Name", email = "you@example.com" }]dependencies = [ "requests>=2.31.0", "fastapi>=0.109.0", "click>=8.1.0",][project.optional-dependencies]redis = ["redis>=5.0"]gui = ["PyQt5>=5.15.0"][dependency-groups]dev = [ "pytest>=8.0.0", "pytest-cov>=5.0.0", "black>=24.0.0", "mypy>=1.8.0", "ruff>=0.3.0",]test = ["httpx>=0.27.0"]docs = [ "sphinx>=7.2.0", "sphinx-rtd-theme>=2.0.0",][project.scripts]mycli = "my_uv_project.cli:main"[project.urls]Homepage = "https://github.com/yourname/my-uv-project"Repository = "https://github.com/yourname/my-uv-project.git"[build-system]requires = ["uv_build>=0.9.0,<0.10.0"]build-backend = "uv_build"# ========== 工具配置 ==========[tool.ruff]line-length = 100select = ["E", "F", "I", "N", "W", "UP"][tool.pytest.ini_options]testpaths = ["tests"]addopts = "-v --cov=my_uv_project --cov-report=html"uv 作为 pip 的直接替代
# uv 完全兼容 pip 接口,可直接替换uv pip install requests # 替代 pip installuv pip install -r requirements.txtuv pip freeze > requirements.txtuv pip listuv pip uninstall requests# 创建虚拟环境uv venv # 替代 python -m venv .venvuv venv --python 3.12# 编译依赖(替代 pip-tools)uv pip compile requirements.in -o requirements.txtuv pip compile pyproject.toml -o requirements.txtuv 与 Poetry/PDM 对比
| 速度 | |||
| 配置格式 | [tool.poetry] | ||
| pip 兼容 | |||
| 锁文件 | |||
| 项目初始化 | |||
| 工具运行 | |||
| GitHub Stars |
选择建议
选 uv 如果你:
追求极致速度,特别是在 CI/CD 中 希望一个工具覆盖 pip + virtualenv + pip-tools + pipx 的所有功能 需要与现有 pip 工作流无缝切换 看重锁文件的跨平台能力
2.5 Hatch:PyPA 官方项目管理工具
Hatch 是 PyPA(Python Packaging Authority)官方推出的现代化项目管理工具。它提供插件系统、环境管理和版本管理等功能。
# 安装pip install hatch# 创建新项目hatch new my-project# 在已有项目中初始化hatch new --init# 创建环境hatch env create# 运行命令hatch run pytesthatch run lint:ruff check .# 构建hatch build# 发布hatch publish# 版本管理hatch version minor # 0.1.0 → 0.2.0hatch version patch # 0.1.0 → 0.1.1Hatch 的最大特点是完全基于 PEP 标准和强大的插件系统(可自定义构建后端、环境类型、版本方案等),适合需要高度定制化的项目。
3. 生成可独立运行的应用程序
当你的用户不想(或不能)安装 Python 环境时,你需要将程序打包成独立的可执行文件。
3.1 打包工具工作原理

3.2 PyInstaller:最通用的应用打包器
PyInstaller 是使用最广泛的 Python 应用打包工具。它通过分析你的入口脚本,递归找到所有依赖,并将其与一个精简的 Python 解释器打包在一起。
安装
pip install pyinstaller# 验证安装pyinstaller --version基本命令
# ========== 基础用法 ==========# 默认打包(生成文件夹,包含所有依赖)pyinstaller script.py# 打包为单个文件(方便分发)pyinstaller --onefile script.py# GUI 应用(Windows/macOS 不显示控制台黑框)pyinstaller --windowed gui_app.py# 指定输出名称pyinstaller --name MyApp script.py# 指定图标pyinstaller --icon=app.ico script.py# 指定输出目录pyinstaller --distpath=./release script.py# ========== 进阶选项 ==========# 添加数据文件(Windows 用分号分隔)# Linux/macOSpyinstaller --add-data "src/config:config" script.py# Windowspyinstaller --add-data "src/config;config" script.py# 添加隐式导入(手动指定分析不到的模块)pyinstaller --hidden-import=module_name script.py# 排除不需要的模块(减小体积)pyinstaller --exclude-module=tkinter script.py# 清理构建文件后打包pyinstaller --clean --onefile script.py# 不压缩(加快启动速度,但文件更大)pyinstaller --noupx --onefile script.py.spec 文件详解
当打包需求变复杂时,推荐使用 .spec 文件:
# -*- mode: python ; coding: utf-8 -*-# myapp.specblock_cipher = None# 1. 分析阶段:分析所有依赖a = Analysis( ['main.py'], # 入口脚本 pathex=[], # 搜索路径 binaries=[], # 额外的二进制文件 datas=[ ('resources/images/*', 'images'), # (源路径, 目标路径) ('resources/config.yaml', '.'), # 配置文件 ('resources/fonts/*.ttf', 'fonts'), ], hiddenimports=['module1','module2.submodule', ], hookspath=[], # 自定义 hook 路径 hooksconfig={}, # hook 配置 runtime_hooks=[], # 运行时 hook excludes=['tkinter', # 排除不需要的模块'matplotlib','IPython', ], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False,)# 2. 合并 Python 字节码pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)# 3. 生成可执行文件exe = EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='MyApp', # 可执行文件名 debug=False, # 调试模式 bootloader_ignore_signals=False, strip=False, # 移除符号信息 upx=True, # 使用 UPX 压缩 upx_exclude=[], # 不压缩的文件 runtime_tmpdir=None, # 临时解压目录 console=True, # 是否显示控制台 disable_windowed_traceback=False, argv_emulation=False, target_arch=None, # 目标架构 codesign_identity=None, # macOS 代码签名 entitlements_file=None, # macOS 权限文件 icon='app.ico', # 应用图标)# 4. 如果打包为文件夹模式,还可以生成 COLLECT# coll = COLLECT(# exe,# a.binaries,# a.zipfiles,# a.datas,# strip=False,# upx=True,# upx_exclude=[],# name='MyApp',# )使用 spec 文件:
# 第一次打包时生成 spec 文件pyinstaller --onefile --name MyApp main.py# 后续直接使用 spec 文件打包pyinstaller myapp.spec# 修改 spec 文件后重新打包pyinstaller --clean myapp.spec处理资源文件
# resource_manager.pyimport sysimport osfrom pathlib import Pathfrom typing import Uniondefget_resource_path(relative_path: Union[str, Path]) -> str:""" 获取资源文件的绝对路径,兼容开发环境和打包环境。 在开发环境中,返回相对于项目根目录的路径。 在 PyInstaller 打包环境中,返回相对于临时解压目录的路径。 Args: relative_path: 相对路径 Returns: 资源的绝对路径 Example: >>> config_path = get_resource_path("config/app.yaml") >>> with open(config_path) as f: ... config = yaml.safe_load(f) """# 检查是否在 PyInstaller 打包环境中if getattr(sys, 'frozen', False):# 打包环境:使用 _MEIPASS(临时解压目录) base_path = sys._MEIPASSelse:# 开发环境:使用项目根目录 base_path = os.path.abspath(".")return os.path.join(base_path, relative_path)defget_app_root() -> str:""" 获取应用程序根目录(可执行文件所在目录)。 Returns: 应用程序根目录的绝对路径 """if getattr(sys, 'frozen', False):# 打包环境:可执行文件所在目录return os.path.dirname(sys.executable)else:# 开发环境:脚本所在目录return os.path.dirname(os.path.abspath(sys.argv[0]))# 使用示例if __name__ == "__main__":# 读取打包到应用内部的配置文件 config_path = get_resource_path("data/config.json")# 读写用户数据(放在可执行文件同目录下) user_data_path = os.path.join(get_app_root(), "user_data.json")常见问题
# 问题1:打包后文件太大# 解决方案:# 1. 在虚拟环境中打包,只安装必要依赖python -m venv build_envsource build_env/bin/activatepip install your-app-depspip install pyinstallerpyinstaller --onefile main.py# 2. 排除不需要的模块pyinstaller --exclude-module=tkinter --exclude-module=matplotlib main.py# 问题2:缺少某些模块# 解决方案:使用 --hidden-importpyinstaller --hidden-import=scipy.special._ufuncs main.py# 问题3:DLL 缺失错误(Windows)# 解决方案:手动添加 DLLpyinstaller --add-binary "C:/path/to/missing.dll;." main.py# 问题4:打包后程序崩溃但开发环境正常# 解决方案:使用 --debug 模式查找问题pyinstaller --debug=all main.py# 问题5:相对导入失败(ImportError: attempted relative import)# 原因:PyInstaller 直接执行脚本,但脚本使用了相对导入(如 from .core import xxx)# 解决方案1:将入口脚本改为绝对导入# from .core import Calculator → from calculator.core import Calculator# 解决方案2:用 -m 模块方式指定入口pyinstaller --onefile -n myapp -m calculator.cli# 解决方案3:创建一个不带相对导入的外层启动脚本 wrapper.py# wrapper.py 内容:from calculator.cli import main; main()3.3 Nuitka:追求性能的编译器
Nuitka 将 Python 代码编译为 C/C++,然后生成原生可执行文件。这不仅能打包,还能提升性能和保护源代码。
安装
pip install nuitka# 验证安装python -m nuitka --version# 确保有 C 编译器# Linux: sudo apt install gcc# macOS: xcode-select --install# Windows: 安装 Visual Studio Build Tools 或 MinGW基本命令
# ========== 基础用法 ==========# 编译为独立文件夹python -m nuitka --standalone script.py# 编译为单个文件python -m nuitka --standalone --onefile script.py# GUI 应用(不显示控制台)# Windowspython -m nuitka --standalone --windows-console-mode=disable gui.py# macOSpython -m nuitka --standalone --macos-create-app-bundle gui.py# 指定输出目录python -m nuitka --standalone --output-dir=build script.py# ========== 优化选项 ==========# 启用链接时优化(LTO)python -m nuitka --standalone --onefile --lto=yes script.py# 移除断言和文档字符串(减少体积)python -m nuitka --standalone --onefile --python-flag=no_site,no_warnings script.py# 使用 Clang 编译器(macOS 上更快)python -m nuitka --standalone --clang script.py# 启用所有优化(生产环境推荐)python -m nuitka --standalone \ --onefile \ --lto=yes \ --remove-output \ --python-flag=no_site,no_warnings \ script.py性能对比
# benchmark.pyimport timedeffibonacci(n):"""递归计算斐波那契数(计算密集型)"""if n <= 1:return nreturn fibonacci(n-1) + fibonacci(n-2)defmain(): start = time.time() result = fibonacci(35) elapsed = time.time() - start print(f"Result: {result}") print(f"Time: {elapsed:.3f}s")if __name__ == "__main__": main()# 运行基准测试python benchmark.py# 结果示例: Time: 2.134s# 使用 Nuitka 编译后运行python -m nuitka --standalone --onefile benchmark.py./benchmark# 结果示例: Time: 1.523s (约 30% 提升)# PyInstaller 打包后运行pyinstaller --onefile benchmark.py./dist/benchmark# 结果示例: Time: 2.145s (几乎无提升)Nuitka 与 PyInstaller 对比
| 原理 | ||
| 性能提升 | ||
| 启动速度 | ||
| 文件大小 | ||
| 编译时间 | ||
| 代码保护 | ||
| 兼容性 | ||
| 成熟度 | ||
| 调试难度 |
使用建议
开发阶段:使用 PyInstaller(快速迭代) 最终发布:使用 Nuitka(性能和保护) 科学计算/数值密集型应用:强烈建议尝试 Nuitka,性能提升可能非常可观
3.4 跨平台打包指南
各平台注意事项
| Windows | .exe | ||
| macOS | .app | ||
| Linux |
macOS 代码签名
# 1. 创建 entitlements.plistcat > entitlements.plist << EOF<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true/> <key>com.apple.security.cs.disable-library-validation</key> <true/></dict></plist>EOF# 2. 签名codesign --deep --force --verify --verbose \ --sign "Developer ID Application: Your Name (XXXXXXXXXX)" \ --entitlements entitlements.plist \ --options runtime \ dist/MyApp.app# 3. 创建 DMGcreate-dmg --volname "MyApp" \ --window-pos 200 120 \ --window-size 800 400 \ --icon-size 100 \ --icon "MyApp.app" 200 190 \ --hide-extension "MyApp.app" \ --app-drop-link 600 185 \"MyApp.dmg" \"dist/MyApp.app"# 4. 公证xcrun notarytool submit MyApp.dmg \ --apple-id "your@email.com" \ --team-id "XXXXXXXXXX" \ --password "@keychain:AC_PASSWORD" \ --waitLinux AppImage 制作
# 1. 创建 AppDir 结构mkdir -p AppDir/usr/binmkdir -p AppDir/usr/share/iconsmkdir -p AppDir/usr/share/applications# 2. 复制可执行文件cp dist/MyApp AppDir/usr/bin/# 3. 创建 desktop 文件cat > AppDir/usr/share/applications/myapp.desktop << EOF[Desktop Entry]Name=MyAppExec=MyAppIcon=myappType=ApplicationCategories=Utility;EOF# 4. 创建 AppRuncat > AppDir/AppRun << 'EOF'#!/bin/bashSELF=$(readlink -f "$0")HERE=${SELF%/*}export PATH="${HERE}/usr/bin/:${PATH}"exec"${HERE}/usr/bin/MyApp""$@"EOFchmod +x AppDir/AppRun# 5. 下载 appimagetool 并制作 AppImagewget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImagechmod +x appimagetool-x86_64.AppImage./appimagetool-x86_64.AppImage AppDir MyApp.AppImage3.5 案例:GUI 应用打包
GUI 应用示例
# gui_app.py"""一个简单的图片查看器,演示 GUI 应用的打包。"""import sysimport osfrom pathlib import Pathfrom typing import Optionaltry:from PyQt5.QtWidgets import ( QApplication, QMainWindow, QLabel, QFileDialog, QVBoxLayout, QWidget, QPushButton, QStatusBar )from PyQt5.QtGui import QPixmap, QIconfrom PyQt5.QtCore import Qtexcept ImportError: print("请安装 PyQt5: pip install PyQt5") sys.exit(1)defresource_path(relative_path: str) -> str:"""获取资源路径,兼容开发环境和打包环境."""if getattr(sys, 'frozen', False): base_path = sys._MEIPASSelse: base_path = os.path.abspath(".")return os.path.join(base_path, relative_path)classImageViewer(QMainWindow):"""图片查看器主窗口."""def__init__(self): super().__init__() self.current_image_path: Optional[str] = None self.init_ui()definit_ui(self):"""初始化 UI.""" self.setWindowTitle("图片查看器 v1.0") self.setGeometry(100, 100, 800, 600)# 设置图标 icon_path = resource_path("resources/icon.png")if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path))# 中央部件 central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout() central_widget.setLayout(layout)# 图片显示标签 self.image_label = QLabel("请打开一张图片") self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet("QLabel { background-color: #f0f0f0; border: 2px dashed #ccc; }" ) layout.addWidget(self.image_label)# 按钮 btn_open = QPushButton("打开图片") btn_open.clicked.connect(self.open_image) layout.addWidget(btn_open)# 状态栏 self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.status_bar.showMessage("就绪")defopen_image(self):"""打开图片文件.""" file_path, _ = QFileDialog.getOpenFileName( self,"选择图片","","图片文件 (*.png *.jpg *.jpeg *.bmp *.gif);;所有文件 (*)" )if file_path: self.load_image(file_path)defload_image(self, file_path: str):"""加载并显示图片.""" pixmap = QPixmap(file_path)if pixmap.isNull(): self.status_bar.showMessage(f"无法加载图片: {file_path}")return# 缩放图片以适应窗口 scaled_pixmap = pixmap.scaled( self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.image_label.setPixmap(scaled_pixmap) self.current_image_path = file_path# 更新状态栏 file_name = os.path.basename(file_path) size = pixmap.size() self.status_bar.showMessage(f"{file_name} | {size.width()}x{size.height()} 像素" )defresizeEvent(self, event):"""窗口大小改变时重新缩放图片.""" super().resizeEvent(event)if self.current_image_path and self.image_label.pixmap(): self.load_image(self.current_image_path)defmain():"""主函数.""" app = QApplication(sys.argv)# 设置应用样式 app.setStyle('Fusion') viewer = ImageViewer() viewer.show() sys.exit(app.exec_())if __name__ == '__main__': main()打包命令
# PyInstaller 打包pyinstaller --onefile \ --windowed \ --name "ImageViewer" \ --icon resources/icon.ico \ --add-data "resources:resources" \ gui_app.py# Nuitka 打包(更推荐用于最终发布)python -m nuitka --standalone \ --onefile \ --windows-console-mode=disable \ --windows-icon-from-ico=resources/icon.ico \ --enable-plugins=pyqt5 \ --include-data-dir=resources=resources \ --output-dir=build \ --remove-output \ --lto=yes \ gui_app.py4. Docker 容器化部署
对于 Web 服务、API、后台任务等,Docker 是最佳选择。
docker run | ||
多阶段构建 Dockerfile
# ========== 构建阶段 ==========FROM python:3.13-slim AS builderWORKDIR /app# 安装系统构建依赖RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ g++ \ && rm -rf /var/lib/apt/lists/*# 复制项目配置文件COPY pyproject.toml README.md ./COPY src/ ./src/# 构建 wheel 包RUN pip install --no-cache-dir build && \ python -m build --wheel && \# 提取依赖信息 pip install --no-cache-dir pip-tools && \ pip-compile --generate-hashes -o requirements.txt pyproject.toml# ========== 运行阶段 ==========FROM python:3.13-slimWORKDIR /app# 安装运行时系统依赖RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ && rm -rf /var/lib/apt/lists/*# 从构建阶段复制 wheelCOPY --from=builder /app/dist/*.whl ./dist/# 安装应用RUN pip install --no-cache-dir dist/*.whl && \ rm -rf dist/# 创建非 root 用户(安全最佳实践)RUN groupadd -r appuser && \ useradd -r -g appuser -m -s /bin/bash appuser && \ chown -R appuser:appuser /app# 切换到非 root 用户USER appuser# 健康检查HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1# 暴露端口EXPOSE8000# 设置环境变量ENV PYTHONUNBUFFERED=1ENV PYTHONDONTWRITEBYTECODE=1# 入口点ENTRYPOINT ["uvicorn", "myapp.main:app", "--host", "0.0.0.0", "--port", "8000"]Docker Compose 编排
# docker-compose.ymlversion:'3.8'services:app:build:context:.dockerfile:Dockerfileimage:myapp:latestcontainer_name:myapprestart:unless-stoppedports:-"8000:8000"environment:-DATABASE_URL=postgresql://user:pass@db:5432/mydb-REDIS_URL=redis://redis:6379/0-LOG_LEVEL=INFOvolumes:-./data:/app/data-./logs:/app/logsdepends_on:db:condition:service_healthyredis:condition:service_healthynetworks:-app-networkhealthcheck:test:["CMD","curl","-f","http://localhost:8000/health"]interval:30stimeout:5sretries:3start_period:10sdb:image:postgres:16-alpinecontainer_name:myapp-dbrestart:unless-stoppedenvironment:-POSTGRES_USER=user-POSTGRES_PASSWORD=pass-POSTGRES_DB=mydbvolumes:-postgres_data:/var/lib/postgresql/datanetworks:-app-networkhealthcheck:test:["CMD-SHELL","pg_isready -U user -d mydb"]interval:10stimeout:5sretries:5redis:image:redis:7-alpinecontainer_name:myapp-redisrestart:unless-stoppedvolumes:-redis_data:/datanetworks:-app-networkhealthcheck:test:["CMD","redis-cli","ping"]interval:10stimeout:5sretries:5volumes:postgres_data:redis_data:networks:app-network:driver:bridge构建与部署
# 构建镜像docker build -t myapp:latest .# 使用 docker-compose 启动docker-compose up -d# 查看日志docker-compose logs -f app# 扩展服务docker-compose up -d --scale app=3# 更新服务docker-compose pull appdocker-compose up -d app# 停止服务docker-compose down# 推送到镜像仓库docker tag myapp:latest registry.example.com/myapp:latestdocker push registry.example.com/myapp:latest5. Conda 打包
Conda 是数据科学领域的事实标准。它不仅管理 Python 包,还管理非 Python 系统依赖。
Conda 包的结构
my-conda-package/├── recipe/│ ├── meta.yaml # 包元数据(核心文件)│ ├── build.sh # Linux/macOS 构建脚本│ └── bld.bat # Windows 构建脚本├── src/ # 源代码│ └── mypackage/│ ├── __init__.py│ └── core.py└── tests/ └── test_package.pymeta.yaml 完整示例
{%setname="mypackage"%}{%setversion="0.1.0"%}package:name:{{name}}version:{{version}}source:path:..build:number:0script:{{PYTHON}}-mpipinstall.--no-deps--no-build-isolation-vventry_points:-mycli=mypackage.cli:main# 跳过特定平台# skip: True # [win]# skip: True # [py<38]requirements:build:-{{compiler('c')}}-{{compiler('cxx')}}host:-python-pip-setuptools-wheelrun:-python-numpy>=1.20.0# 声明非 Python 依赖-pandas>=1.3.0-scipy>=1.7.0-requests>=2.28.0-click>=8.0.0test:imports:-mypackage-mypackage.corecommands:-mycli--help-python-c"from mypackage import Calculator; c=Calculator(); assert c.add(1,2)==3"requires:-pytest-pytest-covabout:home:https://github.com/yourname/mypackagelicense:MITlicense_file:LICENSEsummary:"一个示例 Conda 包"description:| 这是一个演示如何构建 Conda 包的示例项目。doc_url:https://mypackage.readthedocs.iodev_url:https://github.com/yourname/mypackageextra:recipe-maintainers:-yourname构建和发布
# 安装 conda-buildconda install conda-build# 构建包conda-build recipe/# 构建并指定输出目录conda-build --output-folder ./output recipe/# 构建特定 Python 版本conda-build --python 3.12 recipe/# 安装本地构建的包conda install --use-local mypackage# 上传到 Anaconda Cloudanaconda upload /path/to/linux-64/mypackage-0.1.0-py312_0.tar.bz2# 创建自定义频道mkdir -p /path/to/channel/linux-64cp /path/to/mypackage-0.1.0-py312_0.tar.bz2 /path/to/channel/linux-64/conda index /path/to/channel# 然后添加频道: conda config --add channels file:///path/to/channel6. CI/CD 自动化发布
GitHub Actions 完整工作流
# .github/workflows/build-and-release.ymlname:BuildandReleaseon:push:branches:[main]tags:-'v*'# 推送 v 开头的 tag 时触发发布pull_request:branches:[main]workflow_dispatch:# 允许手动触发jobs:# ========== 测试 ==========test:name:Test(Python${{matrix.python-version}})runs-on:ubuntu-lateststrategy:matrix:python-version:['3.12','3.13']steps:-uses:actions/checkout@v4-name:SetupPython${{matrix.python-version}}uses:actions/setup-python@v5with:python-version:${{matrix.python-version}}-name:Installdependenciesrun:| python -m pip install --upgrade pip pip install ".[dev]"-name:Lintwithruffrun:ruffcheck.-name:Typecheckwithmypyrun:mypysrc/-name:Testwithpytestrun:pytest--cov=mypackage--cov-report=xml-name:UploadcoveragetoCodecovuses:codecov/codecov-action@v4with:file:./coverage.xmlfail_ci_if_error:true# ========== 构建 Python 包 ==========build-package:name:BuildPythonPackageneeds:testruns-on:ubuntu-latestif:startsWith(github.ref,'refs/tags/')steps:-uses:actions/checkout@v4-name:SetupPythonuses:actions/setup-python@v5with:python-version:'3.12'-name:Buildpackagerun:| pip install build python -m build-name:Checkpackagerun:| pip install twine twine check dist/*-name:Uploadpackageartifactsuses:actions/upload-artifact@v4with:name:python-packagepath:dist/# ========== 构建可执行文件(多平台) ==========build-binary:name:BuildBinary(${{matrix.os}})needs:testruns-on:${{matrix.os}}if:startsWith(github.ref,'refs/tags/')strategy:matrix:include:-os:ubuntu-latestartifact_name:myapp-linuxasset_name:myapp-linux-os:windows-latestartifact_name:myapp-windowsasset_name:myapp-windows.exe-os:macos-latestartifact_name:myapp-macosasset_name:myapp-macossteps:-uses:actions/checkout@v4-name:SetupPythonuses:actions/setup-python@v5with:python-version:'3.12'-name:Installdependenciesrun:| pip install pyinstaller pip install ".[all]"-name:BuildwithPyInstallerrun:| pyinstaller --onefile \ --name ${{ matrix.asset_name }} \ --add-data "src/mypackage/data:data" \ src/mypackage/cli.py-name:Uploadbinaryartifactuses:actions/upload-artifact@v4with:name:${{matrix.artifact_name}}path:dist/${{matrix.asset_name}}${{matrix.os=='windows-latest'&&'.exe'||''}}# ========== 构建 Docker 镜像 ==========build-docker:name:BuildDockerImageneeds:testruns-on:ubuntu-latestif:startsWith(github.ref,'refs/tags/')steps:-uses:actions/checkout@v4-name:SetupDockerBuildxuses:docker/setup-buildx-action@v3-name:LogintoDockerHubuses:docker/login-action@v3with:username:${{secrets.DOCKER_USERNAME}}password:${{secrets.DOCKER_PASSWORD}}-name:BuildandpushDockerimageuses:docker/build-push-action@v5with:context:.push:truetags:| yourusername/myapp:latest yourusername/myapp:${{ github.ref_name }}# ========== 发布 GitHub Release ==========release:name:CreateGitHubReleaseneeds:[build-package,build-binary,build-docker]runs-on:ubuntu-latestif:startsWith(github.ref,'refs/tags/')steps:-name:Downloadallartifactsuses:actions/download-artifact@v4-name:Displaystructureofdownloadedfilesrun:ls-R-name:CreateReleaseuses:softprops/action-gh-release@v1with:name:Release${{github.ref_name}}body:| ## 🚀 Release ${{ github.ref_name }}### 📦 安装方式**pip安装:**```bashpipinstallmyapp```**Docker运行:**```bashdockerrun-p8000:8000yourusername/myapp:${{github.ref_name}}```### 📥 下载见下方Assets列表。### 📝 更新日志-见[CHANGELOG.md](CHANGELOG.md)draft:falseprerelease:falsefiles:| python-package/* myapp-linux/myapp-linux myapp-windows/myapp-windows.exe myapp-macos/myapp-macosenv:GITHUB_TOKEN:${{secrets.GITHUB_TOKEN}}常见问题
问题 1:打包后找不到数据文件
# ❌ 错误写法config_path = os.path.join(os.path.dirname(__file__), "data/config.json")# ✅ 正确写法import sysimport osdefget_resource_path(relative_path: str) -> str:"""获取资源文件路径,兼容开发和打包环境."""if getattr(sys, 'frozen', False):# PyInstaller 打包后的临时目录 base_path = sys._MEIPASSelse:# 开发环境 base_path = os.path.abspath(".")return os.path.join(base_path, relative_path)config_path = get_resource_path("data/config.json")问题 2:打包后文件体积过大
# 方法 1:在干净的虚拟环境中打包python -m venv build_envsource build_env/bin/activatepip install --no-cache-dir myapppip install pyinstallerpyinstaller --onefile myapp.py# 方法 2:排除不需要的模块pyinstaller --exclude-module=tkinter \ --exclude-module=matplotlib \ --exclude-module=IPython \ --onefile myapp.py# 方法 3:使用 UPX 压缩# 先安装 UPX: https://upx.github.io/pyinstaller --upx-dir=/path/to/upx --onefile myapp.py# 方法 4:Nuitka 配合 LTOpython -m nuitka --standalone --onefile --lto=yes myapp.py问题 3:缺少隐式导入的模块
# PyInstaller 无法分析动态导入# 例如:importlib.import_module(f"plugins.{name}")# 解决方案:手动指定pyinstaller --hidden-import=plugins.plugin1 \ --hidden-import=plugins.plugin2 \ --onefile myapp.py# 或者在 spec 文件中添加a = Analysis( ['main.py'], hiddenimports=['plugins.plugin1', 'plugins.plugin2'],)问题 4:运行时性能问题
# 问题:--onefile 模式启动慢# 原因:每次运行都需要解压到临时目录# 解决方案 1:使用 --onedir(推荐用于频繁运行的程序)pyinstaller --onedir myapp.py# 解决方案 2:使用 Nuitka(启动速度极快)python -m nuitka --standalone --onefile myapp.py# 解决方案 3:使用 --runtime-tmpdir 指定固定解压目录pyinstaller --onefile --runtime-tmpdir /tmp/myapp_cache myapp.py问题 5:代码保护
# 保护等级从低到高:# Level 1: 编译为 .pyc(PyInstaller 默认)# 可被 uncompyle6 等工具反编译# Level 2: 混淆代码# 使用 pyarmorpip install pyarmorpyarmor obfuscate myapp.py# Level 3: 编译为机器码(Nuitka)python -m nuitka --standalone --onefile myapp.py# Level 4: 编译核心模块为 C 扩展(Cython)# setup.pyfrom setuptools import setupfrom Cython.Build import cythonizesetup( ext_modules=cythonize("core.py", language_level="3"),)总结
快速决策指南
选择优先级:
先确定目标用户(开发者还是最终用户) 再确定应用类型(库、桌面应用、Web服务还是数据科学工具) 最后根据具体需求(性能、易用性、标准化)选择具体工具
| 开发 Python 库(速度优先) | ||
| 开发 Python 库(成熟稳定) | ||
| 需要自定义工作流 | ||
| 需要高度定制化 | ||
| 简单脚本分发 | ||
| 商业桌面应用 | ||
| Web 服务部署 | ||
| 数据科学工具 | ||
| CI/CD 自动化 |
工具选择速查表
| 开发 Python 库(速度优先) | ||
| 开发 Python 库(成熟稳定) | ||
| 需要自定义脚本 | ||
| 需要高度定制化 | ||
| 简单脚本分发 | ||
| 商业桌面应用 | ||
| Web 服务部署 | ||
| 数据科学工具 | ||
| CI/CD 自动化 |
建议
不要过度工程化:刚开始时,简单的 pip install -e .或uv pip install -e .就足够开发使用了先发布,后优化:先让你的包/应用能跑起来,再逐步优化打包流程 自动化一切:尽早设置 CI/CD,让构建、测试、发布全自动(uv 在 CI 中速度优势巨大) 文档很重要:好的 README 和安装说明能减少 90% 的用户问题 关注安全:使用 Docker 时用非 root 用户,PyPI 发布时用 API Token
祝你打包顺利!
作者简介:码上工坊,探索用编程为己赋能,定期分享编程知识和项目实战经验。持续学习、适应变化、记录点滴、复盘反思、成长进步。
重要提示:本文主要是记录自己的学习与实践过程,所提内容或者观点仅代表个人意见,只是我以为的,不代表完全正确,欢迎交流讨论。
夜雨聆风