Skip to content

03-包的结构

Python 3.11+

概念铺垫

3.1 什么是包

实际场景

你的项目越来越大:

  • user_utils.py - 用户相关(验证邮箱、格式化手机号)
  • order_utils.py - 订单相关(计算价格、生成订单号)
  • payment_utils.py - 支付相关(处理支付、退款)
  • file_utils.py - 文件相关(读写文件、压缩解压)

20 多个模块文件堆在一起,乱成一团。你想把它们按功能分类整理。

问题:如何用"文件夹"的方式组织多个模块?

概念说明

如果说模块是文件,那么包就是文件夹(目录)。

包是一种通过使用"带点号的模块名"来组织 Python 模块命名空间的方法。

┌─────────────────────────────────────────────────────────────┐
│                    模块 vs 包                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   模块 = 文件(.py 文件)                                   │
│   包   = 文件夹(目录)                                     │
│                                                             │
│   示例:                                                    │
│   my_package/              ← 这是一个包                     │
│   ├── __init__.py          ← 标识这是一个包                 │
│   ├── module_a.py          ← 这是一个模块                   │
│   └── module_b.py          ← 这是一个模块                   │
│                                                             │
│   导入方式:                                                │
│   import my_package           # 导入包                     │
│   import my_package.module_a  # 导入包中的模块              │
│   from my_package import module_a  # 另一种导入方式         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

包的作用

┌─────────────────────────────────────────────────────────────┐
│                    包的作用                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. 模块组织                                               │
│      将相关模块放在同一目录下,便于管理                     │
│                                                             │
│   2. 命名空间                                               │
│      包名作为前缀,避免模块名冲突                           │
│                                                             │
│   3. 代码分层                                               │
│      按功能划分目录,结构清晰                               │
│                                                             │
│   4. 可复用                                                 │
│      包可以独立发布和安装                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

包的目录结构

3.2 基本结构

简单包结构

my_package/
├── __init__.py          # 包初始化文件(必需)
├── module_a.py          # 模块 A
├── module_b.py          # 模块 B
└── subpackage/          # 子包
    ├── __init__.py
    └── module_c.py

实际项目结构

myproject/
├── pyproject.toml       # 项目配置
├── README.md
├── src/
│   └── myproject/
│       ├── __init__.py
│       ├── core/
│       │   ├── __init__.py
│       │   ├── config.py
│       │   └── exceptions.py
│       ├── models/
│       │   ├── __init__.py
│       │   ├── user.py
│       │   └── product.py
│       ├── views/
│       │   ├── __init__.py
│       │   └── home.py
│       └── utils/
│           ├── __init__.py
│           └── helpers.py
└── tests/
    ├── __init__.py
    ├── test_models.py
    └── test_utils.py

__init__.py 文件

3.3 __init__.py 的作用

功能说明

__init__.py 文件标识一个目录为 Python 包。

┌─────────────────────────────────────────────────────────────┐
│                  __init__.py 的作用                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. 标识包                                                 │
│      告诉 Python 这个目录是一个包                           │
│                                                             │
│   2. 初始化代码                                             │
│      包被导入时自动执行                                     │
│                                                             │
│   3. 简化导入                                               │
│      在 __init__.py 中导入子模块,简化外部使用              │
│                                                             │
│   4. 控制导出                                               │
│      通过 __all__ 控制 from package import * 的行为         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Python 3.3+ 的变化

在 Python 3.3 之前,一个目录必须包含 __init__.py 文件,Python 才会把它当成一个包。

从 Python 3.3 开始,引入了"命名空间包"(Namespace Packages),__init__.py 不再是强制要求的。

但是,在实际开发中,强烈建议保留 __init__.py

┌─────────────────────────────────────────────────────────────┐
│              为什么建议保留 __init__.py                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ✓ 兼容性:支持旧版本 Python                               │
│                                                             │
│   ✓ 明确意图:清楚表明这是一个包                            │
│                                                             │
│   ✓ 初始化:可以放置包级初始化代码                          │
│                                                             │
│   ✓ IDE 支持:更好的代码补全和导航                          │
│                                                             │
│   ✓ 导入控制:可以使用 __all__ 控制导出                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

__init__.py 常见用法

用法 1:简化导入路径

python
# my_package/__init__.py
from .module_a import func_a
from .module_b import func_b

# 使用者可以直接导入
from my_package import func_a, func_b
# 而不需要
# from my_package.module_a import func_a

用法 2:初始化代码

python
# my_package/__init__.py
import logging

# 配置日志
logging.getLogger(__name__).addHandler(
    logging.NullHandler()
)

# 初始化资源
_cache: dict[str, Any] = {}

def get_cache() -> dict[str, Any]:
    return _cache

用法 3:控制导出

python
# my_package/__init__.py
from .module_a import func_a
from .module_b import func_b

__all__ = ['func_a', 'func_b']

用法 4:完整包初始化(推荐)

python
# myproject/__init__.py
"""MyProject 工具包"""
from __future__ import annotations

__version__ = "1.0.0"
__all__ = ["user", "order", "payment"]

# 导入子模块供外部使用
from myproject import user
from myproject import order
from myproject import payment

用法 5:延迟导入(避免循环依赖)

python
# myproject/__init__.py
"""MyProject - 一个示例项目

提供核心功能和工具类。
"""
from __future__ import annotations

__version__ = "1.0.0"
__author__ = "张三"

# 延迟导入,避免循环依赖
def __getattr__(name: str) -> Any:
    if name == "User":
        from .models.user import User
        return User
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

子包

3.5 创建子包

目录结构

my_package/
├── __init__.py
├── core/
│   ├── __init__.py
│   ├── config.py
│   └── exceptions.py
├── models/
│   ├── __init__.py
│   ├── user.py
│   └── product.py
└── utils/
    ├── __init__.py
    └── helpers.py

命名空间包

3.6 命名空间包的概念

概念说明

命名空间包(Namespace Package)允许多个目录贡献到同一个包名。

传统包 vs 命名空间包:

┌─────────────────────────────────────────────────────────────┐
│              传统包 vs 命名空间包                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   传统包:                                                   │
│   • 必须有 __init__.py                                      │
│   • 包目录必须在一个位置                                    │
│                                                             │
│   命名空间包:                                               │
│   • 不需要 __init__.py                                      │
│   • 可以分散在多个目录                                      │
│   • 多个项目可以扩展同一个包                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

创建命名空间包

目录结构:

# 项目 1
/path/to/project1/
└── mynamespace/
    └── package_a/
        ├── __init__.py
        └── module_a.py

# 项目 2
/path/to/project2/
└── mynamespace/
    └── package_b/
        ├── __init__.py
        └── module_b.py

隐式命名空间包(Python 3.3+)

不需要 __init__.py,Python 自动识别:

myproject/
└── mynamespace/      # 无 __init__.py
    ├── package_a/
    │   └── __init__.py
    └── package_b/
        └── __init__.py

L1 理解层:会用

导入子包

python
# 导入子包中的模块
from my_package.core import config
from my_package.models.user import User

# 导入子包中的函数
from my_package.utils.helpers import format_date

# 导入整个子包
from my_package import models
user = models.User()

使用命名空间包

python
import sys
sys.path.extend(['/path/to/project1', '/path/to/project2'])

# 两个包都在 mynamespace 下
from mynamespace.package_a import module_a
from mynamespace.package_b import module_b

包的组织建议

目录结构建议

myproject/
├── src/                    # 源代码目录
│   └── myproject/          # 包目录
│       ├── __init__.py
│       ├── core/
│       ├── models/
│       └── utils/
├── tests/                  # 测试目录
├── docs/                   # 文档目录
├── pyproject.toml          # 项目配置
└── README.md

__init__.py 编写建议

python
# myproject/__init__.py
"""MyProject 工具包

提供用户管理、订单处理和支付功能。
"""
from __future__ import annotations

__version__ = "1.0.0"
__author__ = "Your Name"
__all__ = ["UserService", "OrderService", "PaymentService"]

# 延迟导入,避免循环依赖
def __getattr__(name: str) -> Any:
    """延迟导入服务类"""
    services = {
        "UserService": ".services.user.UserService",
        "OrderService": ".services.order.OrderService",
        "PaymentService": ".services.payment.PaymentService",
    }
    if name in services:
        module_path = services[name]
        module = __import__(module_path, fromlist=[name], level=1)
        return getattr(module, name)
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

L2 实践层:用好

推荐做法

做法原因示例
保留 __init__.py 文件明确包意图,兼容旧版本,IDE 友好每个包目录都保留(即使是空文件)
__init__.py 中用相对导入聚合改包名时无需修改内部导入from .module_a import func_a
设置 __all__ 控制包级导出明确公开 API,保护内部实现__all__ = ["UserService", "OrderService"]
使用 __getattr__ 延迟导入避免循环依赖,加快导入速度见上方完整包初始化示例
使用 src/ 布局避免导入混淆,结构清晰src/mypackage/
子包按功能领域划分高内聚低耦合core/, models/, services/, utils/
__init__.py 中定义版本号单一版本来源,便于程序获取__version__ = "1.0.0"

反模式:不要这样做

python
# ❌ 删除 __init__.py 创建命名空间包(非必要场景)
# 没有特殊理由不要删除,IDE 和工具链可能出问题
# my_package/   ← 缺少 __init__.py

# ✅ 保留 __init__.py,即使是空文件
# my_package/
# └── __init__.py   ← 至少保留空文件

# ---

# ❌ 在 __init__.py 中使用绝对导入
# my_package/__init__.py
from my_package.module_a import func_a  # 包改名后需要修改

# ✅ 使用相对导入
# my_package/__init__.py
from .module_a import func_a  # 包改名无需修改

# ---

# ❌ 在 __init__.py 中执行副作用代码
# __init__.py
import logging
logging.basicConfig(level=logging.DEBUG)  # 影响所有导入此包的程序!
_cache = load_expensive_data()           # 导入时阻塞

# ✅ 延迟初始化
# __init__.py
_logger = None
def get_logger():
    global _logger
    if _logger is None:
        _logger = logging.getLogger(__name__)
    return _logger

# ---

# ❌ 包结构扁平化,所有模块堆在一起
myproject/
├── __init__.py
├── user.py
├── product.py
├── order.py
├── payment.py
└── utils.py
# 模块过多,难以定位

# ✅ 按功能分层
myproject/
├── __init__.py
├── models/
│   ├── __init__.py
│   ├── user.py
│   └── product.py
├── services/
│   ├── __init__.py
│   ├── order.py
│   └── payment.py
└── utils/
    ├── __init__.py
    └── helpers.py

适用场景

场景推荐做法原因
小型项目(<5个模块)可以不使用包,直接用模块包增加复杂度,简单场景不需要
中型项目(5-20个模块)使用简单包结构,保留 __init__.py结构清晰,维护成本低
大型项目(>20个模块)src/ 布局 + 多层子包分层清晰,模块化管理
需要跨项目共享代码命名空间包(PEP 420)多项目扩展同一个命名空间
发布到 PyPI 的库标准 src/ 布局 + pyproject.toml符合现代打包规范
循环导入问题__init__.py__getattr__ 延迟导入解决包级别的循环依赖

L3 专家层:深入

Python 如何实现:PEP 420 命名空间包

命名空间包在 Python 3.3+ 通过 PEP 420 实现,不依赖 __init__.py

┌──────────────────────────────────────────────────────────────┐
│              命名空间包(PEP 420)发现机制                     │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   import mynamespace                                         │
│     │                                                        │
│     ▼                                                        │
│   PathFinder.find_module("mynamespace")                      │
│     │                                                        │
│     ├── 遍历 sys.path 中每个目录                            │
│     │                                                        │
│     ├── 目录 /path/A/mynamespace/ 存在?                    │
│     │   ├─ 有 __init__.py → 传统包 → 停止搜索               │
│     │   └─ 无 __init__.py → 标记为命名空间包部分 → 继续搜索 │
│     │                                                        │
│     ├── 目录 /path/B/mynamespace/ 存在?                    │
│     │   └─ 无 __init__.py → 合并到此命名空间               │
│     │                                                        │
│     └── 结果:                                              │
│         mynamespace.__path__ = [                            │
│             "/path/A/mynamespace/",                          │
│             "/path/B/mynamespace/",                          │
│         ]                                                    │
│                                                              │
│   关键差异:                                                 │
│   ┌─────────────────────────────────────────────────────┐   │
│   │                                                      │   │
│   │   传统包 (有 __init__.py):                           │   │
│   │   - __path__ = ["/path/to/package/"]                │   │
│   │   - 找到一个后停止搜索                               │   │
│   │   - 执行 __init__.py 代码                            │   │
│   │                                                      │   │
│   │   命名空间包 (无 __init__.py):                       │   │
│   │   - __path__ = ["/path1/", "/path2/", ...]          │   │
│   │   - 继续搜索所有匹配目录                             │   │
│   │   - 不执行任何初始化代码                             │   │
│   │                                                      │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

__path__ 属性详解

__path__ 是包对象上的一个列表,定义了包中子模块的搜索路径:

┌──────────────────────────────────────────────────────────────┐
│                    __path__ 属性机制                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   当执行 from mypackage import module_x 时:                │
│                                                              │
│   1. Python 找到 mypackage 包对象                            │
│   2. 读取 mypackage.__path__                                │
│   3. 在 __path__ 的每个目录中查找 module_x                  │
│   4. 找到第一个匹配项即停止                                  │
│                                                              │
│   代码等价:                                                 │
│   ┌─────────────────────────────────────────────────────┐   │
│   │  # 传统包                                           │   │
│   │  import mypackage                                   │   │
│   │  print(mypackage.__path__)                          │   │
│   │  # ['/path/to/mypackage/']                          │   │
│   │                                                      │   │
│   │  # 命名空间包                                       │   │
│   │  import mynamespace                                 │   │
│   │  print(mynamespace.__path__)                        │   │
│   │  # ['/proj1/mynamespace/', '/proj2/mynamespace/']   │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                              │
│   __path__ 可被修改(高级用法):                            │
│   # __init__.py 中动态扩展子包搜索路径                      │
│   __path__.append("/extra/path/")                            │
│   # 现在 from mypackage import x 也会在 /extra/path/ 搜索  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

验证代码:

python
import sys
import importlib.util
import pathlib
import tempfile

# --- 演示传统包的 __path__ ---
import json  # 标准库中的包
print("=== 传统包 (json) ===")
print(f"json.__path__: {json.__path__}")
print(f"json 有 __init__.py: {(pathlib.Path(json.__path__[0]) / '__init__.py').exists()}")

# --- 查看包加载的 Spec ---
spec = importlib.util.find_spec("json")
print(f"\njson spec:")
print(f"  name: {spec.name}")
print(f"  submodule_search_locations: {spec.submodule_search_locations}")
# submodule_search_locations 就是 __path__ 的来源

# --- 创建命名空间包示例 ---
# Python 3.3+ 自动识别无 __init__.py 的目录为命名空间包
with tempfile.TemporaryDirectory() as tmpdir:
    # 创建两个分散的命名空间包目录
    ns_dir1 = pathlib.Path(tmpdir) / "ns_pkg" / "sub_a"
    ns_dir2 = pathlib.Path(tmpdir) / "ns_pkg" / "sub_b"
    ns_dir1.mkdir(parents=True)
    ns_dir2.mkdir(parents=True)
    
    # 加入搜索路径
    sys.path.insert(0, tmpdir)
    
    import importlib
    # 注意:需要在每个子目录放置至少一个 Python 文件,否则命名空间包不会创建
    (ns_dir1 / "module_a.py").write_text("VALUE_A = 1")
    (ns_dir2 / "module_b.py").write_text("VALUE_B = 2")
    
    # 导入子包(这会触发命名空间包的发现)
    from ns_pkg.sub_a import module_a
    from ns_pkg.sub_b import module_b
    print(f"\n=== 命名空间包 ===")
    print(f"ns_pkg.__path__: {ns_pkg.__path__}")
    print(f"ns_pkg 有 __init__.py: {hasattr(ns_pkg, '__init__')}")  # 命名空间包没有
    print(f"module_a.VALUE_A: {module_a.VALUE_A}")
    print(f"module_b.VALUE_B: {module_b.VALUE_B}")

包的加载流程(传统包 vs 命名空间包)

python
# 模拟包加载过程的简化代码
def find_package(name: str, paths: list[str]) -> tuple:
    """模拟 PathFinder 查找包的过程"""
    results = []
    for path in paths:
        package_dir = pathlib.Path(path) / name
        if package_dir.is_dir():
            has_init = (package_dir / "__init__.py").exists()
            results.append((str(package_dir), has_init))
    return tuple(results)

# 传统包:第一个有 __init__.py 的目录
print(find_package("json", sys.path[:3]))
# 输出类似: (('/usr/lib/python3.12/json/', True),)

# 命名空间包:多个无 __init__.py 的目录
print(find_package("mynamespace", ["/proj1", "/proj2"]))
# 输出类似: (('/proj1/mynamespace/', False), ('/proj2/mynamespace/', False))

性能考量

操作复杂度说明
导入传统包O(1) + O(n) 执行 __init__.py找到第一个匹配目录即停止
导入命名空间包O(m) 遍历全部 sys.pathm = sys.path 条目数,无 __init__.py 执行
package.__path__ 读取O(1)直接返回列表引用
从包导入子模块O(p) 遍历 __path__p = __path__ 中的目录数
__getattr__ 延迟导入O(1) 首次,O(导入) 按需仅在属性不存在时触发
深层子包嵌套 (a.b.c.d)O(层数) 逐级加载每层都需初始化父包

知识关联

┌──────────────────────────────────────────────────────────────┐
│                    包的结构 知识关联图                         │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   本章(包的结构)                                           │
│     │                                                        │
│     ├── __init__.py 聚合导入 ─► 04-导入机制(相对导入)      │
│     │                                                        │
│     ├── __all__ 包级导出 ────► 01-模块基础(模块 __all__)  │
│     │                                                        │
│     ├── PEP 420 命名空间包 ──► 04-导入机制(Finder/Loader)  │
│     │                                                        │
│     ├── __path__ 属性 ──────► 04-导入机制(sys.meta_path)  │
│     │                                                        │
│     ├── 包目录结构 ─────────► 06-发布自己的包(pyproject)   │
│     │                                                        │
│     ├── __getattr__ 延迟导入 ─► 02-自定义模块(懒加载)      │
│     │                                                        │
│     └── 子包嵌套 ───────────► 04-导入机制(绝对/相对导入)    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

本章小结

┌─────────────────────────────────────────────────────────────┐
│                      包的结构 知识要点                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   包的概念:                                                 │
│   ✓ 包是包含模块的目录                                      │
│   ✓ 用于组织代码、避免命名冲突                              │
│                                                             │
│   目录结构:                                                 │
│   ✓ 传统包必须有 __init__.py                                │
│   ✓ 可以有子包                                              │
│                                                             │
│   __init__.py:                                              │
│   ✓ 标识目录为包                                            │
│   ✓ 简化导入路径                                            │
│   ✓ 初始化代码                                              │
│   ✓ 控制 __all__ 导出                                       │
│                                                             │
│   Python 3.3+ 命名空间包:                                   │
│   ✓ 不需要 __init__.py                                      │
│   ✓ 可以分散在多个目录                                      │
│   ✓ 但实际开发仍建议保留 __init__.py                        │
│                                                             │
│   最佳实践:                                                 │
│   ✓ 使用 src/ 目录结构                                      │
│   ✓ __init__.py 中定义 __version__                          │
│   ✓ 延迟导入避免循环依赖                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘