本节目标:理解「包」的本质、
__init__.py到底起什么作用,掌握from 包.模块 import 名字的逐层解析过程,并能写出正确、不踩坑的相对导入与__all__。
适用版本:Python 3.12+(实测 3.14.6)
5.2 包、init.py 与相对导入
上一节 5.1 import 机制与模块搜索路径
讲清了「单个模块」如何被查找和加载。但一个项目动辄几十上百个文件,全堆在一个目录里根本管不住。Python 的解法是包(package):用目录把相关模块分组,用点号(.)表达层级。本节把包的定义、__init__.py、相对导入三件事一次讲透。
一、什么是包:有 init.py 的目录
最简单的定义是:一个包含 __init__.py 的目录就是一个常规包(regular package)。目录名就是包名,目录里的 .py 文件就是模块。
考虑一个电商项目的最小结构:
shop/
├── __init__.py
└── core/
├── __init__.py
└── pricing.py
对应地,shop 是顶层包,shop.core 是子包,shop.core.pricing 是一个模块。三者通过点号连接,形成模块的全限定名(fully qualified name)。
导入时,Python 会按点号逐层进入目录。这一点和文件系统路径是直接对应的:
| 模块全名 | 对应文件 |
|---|---|
shop | shop/__init__.py |
shop.core | shop/core/__init__.py |
shop.core.pricing | shop/core/pricing.py |
二、init.py 的作用:空的还是放东西
__init__.py 最容易误解的一点是:它不是必须写内容,但它的存在本身就有意义——它告诉 Python「这个目录是一个包」。如果 __init__.py 是空的,那么导入这个包时,Python 只是执行一个空文件,什么也不做。
但 __init__.py 里可以放代码,而且放进去的代码会在包第一次被导入时执行一次。下面这个例子让每个文件都打印一行,观察执行顺序。
# shop/__init__.py
print("[shop/__init__.py] 执行")
from .core.pricing import price_with_tax # 预导入
__all__ = ["price_with_tax", "SHOP_NAME"]
SHOP_NAME = "示例商城"
# shop/core/__init__.py
print("[shop/core/__init__.py] 执行")
# shop/core/pricing.py
print("[shop/core/pricing.py] 执行")
TAX_RATE = 0.13
def price_with_tax(price):
return round(price * (1 + TAX_RATE), 2)
# demo_pkg.py
import shop
print("SHOP_NAME:", shop.SHOP_NAME)
print("price:", shop.price_with_tax(100))
真实输出:
[shop/__init__.py] 执行
[shop/core/__init__.py] 执行
[shop/core/pricing.py] 执行
SHOP_NAME: 示例商城
price: 113.0
执行顺序清楚地展示了「逐层」:导入 shop → 执行 shop/__init__.py → 其中 from .core.pricing import ... 触发导入 shop.core → 执行 shop/core/__init__.py → 再导入 shop.core.pricing → 执行 pricing.py。全部完成后才回到 demo_pkg.py 的下一行。
__init__.py 里放什么,是一个设计决策。常见做法有三类:
| 做法 | 例子 | 优点 | 缺点 |
|---|---|---|---|
| 留空 | 只有文件本身 | 零副作用,导入快 | 使用者要写全路径 |
| 预导入公开 API | from .core.pricing import price_with_tax | 使用方便,shop.price_with_tax 即可 | 导入包会连带加载子模块 |
| 放元数据 | __version__ = "1.0.0" | 版本可查 | 无 |
三、from package.module import name 的逐层解析
from shop.core.pricing import price_with_tax 这一行,Python 做了四件事:
- 从
sys.path里找到shop这个包并执行shop/__init__.py; - 在
shop里找到core子包并执行shop/core/__init__.py; - 在
shop.core里找到pricing模块并执行shop/core/pricing.py; - 从
pricing模块对象上取出属性price_with_tax,绑定到当前名字空间。
任何一步失败,报错都不一样,这正好可以用来定位问题:
| 报错 | 含义 |
|---|---|
ModuleNotFoundError: No module named 'shop' | 第 1 步就失败:sys.path 里找不到这个包 |
ModuleNotFoundError: No module named 'shop.core' | 找到了 shop,但里面没有 core |
ImportError: cannot import name 'price_with_tax' from 'shop.core.pricing' | 模块找到了,但里面没有这个名字 |
最后一种最常见,通常是把名字拼错,或者名字还没定义就导入(循环导入的典型症状,5.3 会讲)。
四、相对导入:. 与 .. 的规则与硬约束
包内部的模块之间互相导入时,可以用相对导入,用点号表示「相对于当前模块所在的位置」:
.表示当前包;..表示上一级包;- 每多一个点,就向上一级。
# shop/core/pricing.py 里可以这样引用同级的另一个模块
from . import discount # 导入 shop.core.discount
from .discount import apply # 从 shop.core.discount 导入 apply
from ..util import round_money # 导入 shop.util(上一级)
相对导入有一条硬约束:它只能在包内使用,不能用于顶层脚本。原因在 5.1 节已经埋下——相对导入需要知道「当前模块属于哪个包」,这依赖解释器为模块建立的父包信息。当模块被当作脚本直接运行时,父包信息是缺失的。
用一个最小实验复现:
# app/util.py
def add(a, b):
return a + b
# app/main.py
from .util import add # 相对导入
print("1 + 2 =", add(1, 2))
直接运行会失败:
python3 app/main.py
File "/private/tmp/py5/app/main.py", line 1, in <module>
from .util import add # 相对导入
^^^^^^^^^^^^^^^^^^^^^
ImportError: attempted relative import with no known parent package
改用 -m 把 main.py 当作包的一部分来运行,就成功了:
python3 -m app.main
1 + 2 = 3
这就是一条实践准则:如果一个文件里写了相对导入,它就只能用 python -m 包.模块 运行,不能直接 python 文件.py。两者不可兼得,这也是很多新手「代码没问题但一运行就报错」的原因。
相对导入和绝对导入怎么选?社区的主流意见是:包内部用相对导入,跨包用绝对导入。相对导入的优点是重构目录时不用改导入语句,缺点是读代码时不容易看出模块在哪。
五、all 与 from pkg import *
from package import * 会把包里的名字批量导入当前名字空间。但「哪些名字算公开」需要一个声明,这就是 __all__——一个字符串列表。
# store/__init__.py
__all__ = ["public_fn"]
def public_fn():
return "公开"
def _private_fn():
return "私有"
def another_fn():
return "另一个"
用 exec 捕获星号导入的结果,看看哪些名字进来了:
ns = {}
exec("from store import *", ns)
print("导入进来的名字:", sorted(k for k in ns if not k.startswith("__")))
真实输出:
导入进来的名字: ['public_fn']
只有 __all__ 里列出的 public_fn 被导入。another_fn 虽然不以下划线开头,但因为不在 __all__ 里,同样被挡住;_private_fn 本来就被下划线规则排除。
两条规则要记清:
| 场景 | 星号导入带进来的名字 |
|---|---|
定义了 __all__ | 只带 __all__ 里的名字 |
没定义 __all__ | 所有不以 _ 开头的名字 |
工程上,import * 应当尽量避免——它污染名字空间、让依赖关系变得不可见。__all__ 更适合作为「声明公开 API」的文档,而不是鼓励别人用星号导入。
六、importlib 动态导入
有时模块名要到运行时才知道(比如按配置加载插件、按字符串找后端实现)。这时 import 语句无能为力,要用 importlib.import_module。
import importlib
mod = importlib.import_module("shop.core.pricing")
print("动态拿到:", mod.price_with_tax(200))
真实输出:
[shop/__init__.py] 执行
[shop/core/__init__.py] 执行
[shop/core/pricing.py] 执行
动态拿到: 226.0
注意两点。第一,import_module 接受的是字符串,所以可以做 import_module(f"backends.{name}") 这类动态拼接。第二,它同样走 sys.modules 缓存——如果模块已经导入过,直接返回缓存对象,不会再执行一遍。相对路径也可以传,但必须带 package 参数:importlib.import_module(".pricing", package="shop.core")。
七、init.py 里预导入的利弊
回到第二节的例子,shop/__init__.py 里写了 from .core.pricing import price_with_tax,这样使用者只要 import shop 就能直接用 shop.price_with_tax。这是预导入(re-export)。
它是一把双刃剑:
| 好处 | 代价 |
|---|---|
使用方接口更短,shop.price_with_tax 而不是 shop.core.pricing.price_with_tax | 只要 import shop,整个子模块树都被加载,导入变慢 |
| 公开 API 集中在一处,便于文档化 | 容易掩盖模块边界,产生意外的循环导入 |
| 改名时只需改一处 | __init__.py 里的导入有副作用时,任何导入方都会被牵连 |
启动时间不是小事。包越大,import 顶层包 连带加载的子模块越多,程序启动越慢。5.3 节会用 python -X importtime 把这件事量化,并给出惰性导入的几种缓解手段。一个务实的建议是:只预导入高频使用、且加载代价小的核心接口,其余保持惰性。
小结
本节围绕「包」这一组织单位,把关键机制总结如下:
- 包 = 含
__init__.py的目录,目录名即包名,模块全名用点号逐层连接。 __init__.py在包首次导入时执行一次,留空最轻,预导入最方便也最贵。from 包.模块 import 名字逐层解析,不同层的失败给出不同报错,可用于定位问题。- 相对导入只能在包内使用:
.表示当前包、..表示上一级;写了相对导入的文件只能用python -m运行。 __all__控制import *的公开名字,没定义时按「不以_开头」规则。importlib.import_module支持运行时按字符串动态导入,同样享受sys.modules缓存。
本节讲了正常的、良性的导入关系。但真实项目里,模块之间经常互相依赖,于是出现循环导入;还有些目录故意不放 __init__.py。下一节 5.3 循环导入、命名空间包与惰性导入
将处理这三类棘手情况,并给出可落地的修法。
阅读导航:上一节:5.1 import 机制与模块搜索路径 · 下一节:5.3 循环导入、命名空间包与惰性导入 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。