《Python编程入门》5.2 包、__init__.py 与相对导入

本节聚焦「包」这种组织模块的方式:有 __init__.py 的目录发生了什么、空文件与写满代码的 __init__.py 各有什么代价、from 包.模块 import 名字 如何逐层解析、相对导入「只能在包内使用」的硬约束从何而来。还讲了 __all__ 对 import * 的控制、用 importlib 动态导入,以及预导入在易用性与启动时间间的取舍。

本节目标:理解「包」的本质、__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 会按点号逐层进入目录。这一点和文件系统路径是直接对应的:

模块全名对应文件
shopshop/__init__.py
shop.coreshop/core/__init__.py
shop.core.pricingshop/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 里放什么,是一个设计决策。常见做法有三类:

做法例子优点缺点
留空只有文件本身零副作用,导入快使用者要写全路径
预导入公开 APIfrom .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 做了四件事:

  1. 从 sys.path 里找到 shop 这个包并执行 shop/__init__.py;
  2. 在 shop 里找到 core 子包并执行 shop/core/__init__.py;
  3. 在 shop.core 里找到 pricing 模块并执行 shop/core/pricing.py;
  4. 从 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 把这件事量化,并给出惰性导入的几种缓解手段。一个务实的建议是:只预导入高频使用、且加载代价小的核心接口,其余保持惰性。

小结

本节围绕「包」这一组织单位,把关键机制总结如下:

  1. 包 = 含 __init__.py 的目录,目录名即包名,模块全名用点号逐层连接。
  2. __init__.py 在包首次导入时执行一次,留空最轻,预导入最方便也最贵。
  3. from 包.模块 import 名字 逐层解析,不同层的失败给出不同报错,可用于定位问题。
  4. 相对导入只能在包内使用:. 表示当前包、.. 表示上一级;写了相对导入的文件只能用 python -m 运行。
  5. __all__ 控制 import * 的公开名字,没定义时按「不以 _ 开头」规则。
  6. importlib.import_module 支持运行时按字符串动态导入,同样享受 sys.modules 缓存。

本节讲了正常的、良性的导入关系。但真实项目里,模块之间经常互相依赖,于是出现循环导入;还有些目录故意不放 __init__.py。下一节 5.3 循环导入、命名空间包与惰性导入 将处理这三类棘手情况,并给出可落地的修法。

阅读导航:上一节:5.1 import 机制与模块搜索路径 · 下一节:5.3 循环导入、命名空间包与惰性导入 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时