当前位置: 首页 > news >正文

别再手动写packages了!用setuptools的find_packages()自动打包你的Python多模块项目

别再手动维护Python包列表了!用find_packages()实现智能模块发现

当你第一次尝试打包一个包含多个子模块的Python项目时,setup.py文件中那个看似简单的packages参数可能会让你抓狂。想象一下这样的场景:你的项目结构随着开发不断变化,每次新增一个子包,你都得记得回来更新这个列表。更糟的是,如果你不小心漏掉了一个包,用户在安装时就会遇到"ModuleNotFoundError"——而你可能要到用户反馈时才会发现这个问题。

1. 为什么手动维护packages列表是个糟糕的主意

在典型的Python项目中,随着功能模块的增加,目录结构往往会变得越来越复杂。比如一个机器学习项目可能包含以下子包:

project/ ├── data/ │ ├── __init__.py │ ├── loaders.py │ └── preprocessors.py ├── models/ │ ├── __init__.py │ ├── neural_net.py │ └── traditional.py ├── utils/ │ ├── __init__.py │ ├── logger.py │ └── metrics.py └── tests/ ├── __init__.py ├── test_data.py └── test_models.py

手动维护packages列表时,你的setup.py可能是这样的:

from setuptools import setup setup( name="my_project", version="0.1", packages=[ "project", "project.data", "project.models", "project.utils", # 很容易忘记添加新创建的包 ], # 其他参数... )

这种方式的三大致命缺陷:

  1. 维护成本高:每次新增子包都需要手动更新列表
  2. 容易出错:遗漏包会导致安装后无法导入
  3. 包含不需要的包:可能不小心包含测试包或示例代码

提示:根据PyPA的打包指南,手动指定packages已被列为不推荐做法,特别是在项目结构复杂的情况下。

2. find_packages()的工作原理与基本用法

setuptools提供的find_packages()函数通过扫描项目目录自动发现Python包,解决了上述所有问题。它的工作逻辑非常直观:

  1. 递归扫描指定目录(默认为setup.py所在目录)
  2. 识别所有包含__init__.py的目录(即Python包)
  3. 返回这些包的导入路径列表

最基本的用法简单到令人发指:

from setuptools import setup, find_packages setup( name="my_project", version="0.1", packages=find_packages(), # 自动包含所有子包! )

这个简单的改变带来的好处立竿见影:

  • 自动包含新包:新建的子包会被自动发现
  • 减少错误:不再有遗漏包的问题
  • 一致性保证:打包行为与项目结构始终保持同步

2.1 理解find_packages的扫描规则

为了更好地控制打包过程,我们需要了解find_packages()的扫描行为:

  • 包含规则

    • 目录必须包含__init__.py文件
    • 包名必须是有效的Python标识符(不能以数字开头等)
  • 排除规则

    • 默认排除名称中包含"test"或"tests"的目录
    • 不扫描以点(.)开头的隐藏目录

下表对比了手动指定与自动发现的差异:

特性手动指定packagesfind_packages()
维护成本高(需手动更新)零维护
准确性依赖开发者记忆自动保证
灵活性静态列表动态适应项目变化
测试包处理可能意外包含默认排除
新开发者友好度低(需了解全部结构)高(自动适应)

3. 高级过滤:精确控制包含哪些包

虽然find_packages()的默认行为已经能解决大部分问题,但有时我们需要更精细的控制。比如:

  • 排除特定的子包(如示例代码、实验性功能)
  • 只包含特定模式的包
  • 处理特殊的目录结构

3.1 使用exclude参数过滤不需要的包

最常见的需求是排除测试代码和示例:

packages=find_packages(exclude=["tests", "tests.*", "examples", "examples.*"])

这个配置会:

  • 排除顶级tests目录
  • 排除所有tests的子包(如tests.unit)
  • 排除examples目录及其子包

注意:exclude模式使用Unix shell风格的通配符,所以tests.*可以匹配所有tests的子包。

3.2 使用include参数指定包含模式

当项目结构特别复杂时,你可能想明确指定包含哪些包:

packages=find_packages(include=["project", "project.*"])

这会:

  • 包含顶级project包
  • 包含所有project的子包
  • 排除其他所有包

3.3 处理特殊目录结构

有时项目可能采用非标准布局,比如:

src/ └── my_pkg/ ├── __init__.py └── submodule.py

这时需要指定查找的根目录:

packages=find_packages(where="src")

并在setup.py中配置package_dir告诉setuptools如何映射:

setup( # ... packages=find_packages(where="src"), package_dir={"": "src"}, )

4. 实战:从手动到自动的完整迁移案例

让我们通过一个真实场景演示如何将现有项目从手动packages迁移到find_packages()。

4.1 初始项目结构

假设我们有一个Flask web应用项目:

flask_app/ ├── app/ │ ├── __init__.py │ ├── models/ │ │ ├── __init__.py │ │ ├── user.py │ │ └── product.py │ ├── routes/ │ │ ├── __init__.py │ │ ├── api.py │ │ └── web.py │ └── utils/ │ ├── __init__.py │ ├── auth.py │ └── decorators.py ├── tests/ │ ├── __init__.py │ ├── test_models.py │ └── test_routes.py ├── scripts/ │ ├── __init__.py │ └── deploy.py └── setup.py

4.2 原始setup.py(手动维护)

from setuptools import setup setup( name="flask_app", version="1.0", packages=[ "app", "app.models", "app.routes", "app.utils", # 忘记添加新创建的包怎么办? # scripts目录应该包含吗? ], )

4.3 改进后的setup.py(自动发现)

from setuptools import setup, find_packages setup( name="flask_app", version="1.0", packages=find_packages(exclude=["tests*", "scripts*"]), # 明确排除测试和脚本目录 install_requires=[ "flask>=2.0", "sqlalchemy>=1.4", ], )

4.4 验证打包结果

安装前检查哪些包会被包含:

python setup.py --verbose check

构建源码发布包并查看内容:

python setup.py sdist tar -tzvf dist/flask_app-1.0.tar.gz

你应该看到类似这样的输出,确认只包含了app及其子包:

drwxr-xr-x 0 user group 0 Jan 1 12:00 flask_app-1.0/ drwxr-xr-x 0 user group 0 Jan 1 12:00 flask_app-1.0/app/ drwxr-xr-x 0 user group 0 Jan 1 12:00 flask_app-1.0/app/models/ drwxr-xr-x 0 user group 0 Jan 1 12:00 flask_app-1.0/app/routes/ drwxr-xr-x 0 user group 0 Jan 1 12:00 flask_app-1.0/app/utils/

5. 常见问题与最佳实践

5.1 为什么我的包没有被正确包含?

如果发现某些包没有被find_packages()包含,检查以下几点:

  1. 确认存在__init__.py

    • 没有__init__.py的目录不会被识别为Python包
    • 即使是空文件也必须存在
  2. 检查exclude模式

    • 确认没有意外排除了需要的包
    • 模式匹配是大小写敏感的
  3. 验证目录结构

    • 确保包位于setup.py所在的目录或子目录
    • 对于非标准布局,记得设置where参数

5.2 性能考虑:大型项目中的优化

对于包含数百个子包的超大型项目,find_packages()的递归扫描可能会带来明显的性能开销。这时可以考虑:

  • 使用include限制范围

    find_packages(include=["core*", "utils*"])
  • 拆分项目

    • 考虑将大型项目拆分为多个较小的包
    • 使用namespace packages组织代码

5.3 与构建工具链的集成

现代Python项目通常使用更高级的构建工具,这些工具也与find_packages()良好集成:

Poetry配置示例

[tool.poetry] packages = [ { include = "app", from = "src" }, { include = "app.*", from = "src" } ]

Flit配置示例

[build-system] requires = ["flit_core>=3.2"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "app"

5.4 测试你的打包配置

建立自动化测试验证打包行为是个好习惯:

  1. 创建一个简单的测试脚本:
import sys import pkgutil def test_imports(): for _, modname, _ in pkgutil.iter_modules(["app"]): __import__(f"app.{modname}")
  1. 在CI/CD流水线中添加打包验证步骤:
# .github/workflows/test.yml jobs: test-packaging: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - run: pip install . - run: python -m pytest tests/test_packaging.py

6. 超越find_packages:现代Python打包实践

虽然find_packages()解决了包发现的问题,但现代Python打包还有更多最佳实践值得关注:

6.1 使用pyproject.toml替代setup.py

PEP 517和PEP 518引入了pyproject.toml作为新的标准配置文件:

[build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" [project] name = "my_project" version = "0.1.0" dependencies = [ "requests>=2.25", "numpy>=1.20", ] [tool.setuptools.packages] find = {} # 使用默认的find_packages()

6.2 打包非Python文件

如果你的包需要包含数据文件(如模板、静态资源等),需要使用package_data或MANIFEST.in:

setup( # ... package_data={ "app": ["templates/*.html", "static/*.css"], }, include_package_data=True, )

6.3 命名空间包的处理

对于跨多个分发版的共享命名空间,使用PEP 420风格的命名空间包:

project/ ├── src/ │ ├── company/ │ │ └── product/ │ │ ├── __init__.py │ │ └── module.py

setup.py配置:

setup( packages=find_packages(where="src"), package_dir={"": "src"}, namespace_packages=["company"], )

7. 从理论到实践:真实项目中的决策点

在实际项目中采用find_packages()时,有几个关键决策需要考虑:

  1. 测试代码的处理方式

    • 完全排除(推荐用于生产部署)
    • 作为额外依赖包含(tests_require参数)
    • 打包为独立的分发包
  2. 示例代码的打包策略

    • 与主包一起分发
    • 作为独立示例项目
    • 完全排除,仅保留在文档中
  3. 开发工具的选择

    • 经典setuptools(兼容性好)
    • Poetry(更适合应用开发)
    • Flit(适合纯Python小包)

在最近的一个Web服务项目中,我们采用了这样的配置:

setup( name="web_service", version="0.1", packages=find_packages(exclude=["tests*", "examples*"]), extras_require={ "dev": [ "pytest>=6.0", "black>=21.0", ], "docs": [ "sphinx>=4.0", "furo>=2021.0", ], }, python_requires=">=3.8", )

这种配置实现了:

  • 生产安装只包含运行时必需的包
  • 开发者可以通过pip install -e .[dev]获取测试工具
  • 明确指定了Python版本要求
http://www.jsqmd.com/news/683744/

相关文章:

  • 展讯A16摄像头插值到非代码中预设值时处理方法
  • 网络安全实战干货:从个人防护到企业防护,全场景避坑指南
  • 告别IP盲猜:为你的STM32设备加上“网络身份证”(基于LwIP 2.1.2的HostName与DHCP深度集成教程)
  • 2026年如何部署OpenClaw?8分钟华为云保姆级安装及百炼Coding Plan步骤
  • STM32CubeIDE新手必知的10个快捷键,效率提升不止一倍(附重定义printf避坑指南)
  • Altium Designer 导出Gerber和坐标文件保姆级教程(附常见报错排查)
  • 什么是数据库?什么是关系数据库?什么是非关系型数据库?
  • 告别手动推导噩梦:用Matlab符号工具箱快速搞定球坐标拉普拉斯算子转换
  • 告别Demo版限制:手把手教你搞定CANoe 17.0的License激活与疑难杂症排查
  • 高效构建由对称子矩阵组成的三维数组
  • Claude-Opus-47-VS-GLM-51-2026编程能力王者之争
  • 区块链与AI融合:10大产业变革深度解析
  • Qt信号量QSemaphore避坑指南:tryAcquire非阻塞调用、release过量释放,这些多线程‘暗雷’你踩过吗?
  • 猫抓浏览器扩展:轻松捕获网页媒体资源的终极指南
  • Python变量相关性分析:原理、实现与实战应用
  • 别再写硬编码了!MyBatis-Plus的apply方法,这样用才安全又灵活(附日期查询实战)
  • 1篇5章2节:macOS 必备开源包管理器 Homebrew
  • 生化危机8修改器 风灵月影 支持最新版本
  • Element UI 表格合并踩坑记:从官网示例到真实业务场景的完整避坑指南
  • ROS+Catkin项目如何正确生成compile_commands.json?让clangd在VSCode里精准补全
  • Python 工程化开发与性能优化实践
  • 别再到处找数据了!手把手教你从三大GWAS数据库(IEU、MiBioGen、FinnGen)一键下载与清洗
  • 光学设计避坑指南:反射棱镜选型、展开与光轴计算的3个关键步骤
  • 前端性能优化实战:用FormData和axios拦截器改造el-upload,轻松合并上传请求
  • 告别内核编译:手把手教你用Linux configfs动态配置USB音频设备(UAC2.0实战)
  • 麒麟系统更新后输入法消失?别慌,一个终端命令帮你找回(附fcitx修复详解)
  • 选择电容的额定电压,核心依据
  • 告别手动涂色!LaTeX进阶技巧:用xpatch动态控制特定参考文献的样式(以颜色为例)
  • S04|子代理:给 Agent 开 “独立小房间”,上下文不乱、主线不飘
  • OFA-VE部署教程:使用Poetry管理依赖,构建可复现的Python3.11环境