整体总结
- 导入即执行 :
import会自顶向下执行__init__.py的全部顶层代码(含级联导入),但仅首次导入执行一次,之后走sys.modules缓存 __all__是公共 API 白名单 :硬性控制import *的范围,软性向人、IDE、类型检查器声明包的公开接口,但不提供任何真正的访问限制- 包中的各种对象,包括普通对象、类名、函数名、甚至包名等都是属于 dict 对象的 key-value 结构存储的,所以他们可以被修改
修改导入的对象
模块的命名空间是一个 dict
- 模块中的一切顶层名字(变量、函数、类、子模块等)都存储在该模块对象的
__dict__中(可通过vars(a)查看) - 名字只是指向对象的引用,因此可以随时重新绑定(即 monkey patch)
通过模块属性访问:动态查找,修改全局可见
这行代码把
a.__dict__["b"]重新绑定到c1
2import a
a.b = c此后所有通过
a.b这种属性访问方式读取的代码(无论在哪个模块),都会查到新值c- 因为每次
a.b都是运行时在模块 dict 中的一次实时查找
- 因为每次
from a import b:一次性值拷贝,之后与 a.b 无关
from a import b导入的本质:- 等价于
b = a.b:把当时a.__dict__["b"]指向的对象,绑定到当前模块的本地名字b上 - 此后:
- 若执行
a.b = c,只改变了a模块 dict 中的绑定;本地的b仍指向原对象,不受影响 - 反之亦然:给本地
b重新赋值也不影响a.b
- 若执行
- 等价于
- 两个名字是独立的引用 ,只是曾经指向同一个对象
导入包时执行了哪些代码
核心规则:导入 = 执行
- Python 的
import不是简单的”声明引用”,而是真正执行代码 - 当执行
import mypkg时,发生以下事情:- 1)查找 :解释器按
sys.path顺序查找名为mypkg的包(目录)或模块(.py文件) - 2)检查缓存 :先查
sys.modules,如果已导入过则直接复用,不会重复执行 - 3)执行 :首次导入时,自顶向下完整执行
mypkg/__init__.py中的所有模块级代码 - 4)绑定 :把生成的模块对象绑定到当前命名空间的
mypkg名字上
- 1)查找 :解释器按
哪些代码会被执行
__init__.py中所有模块级(顶层)语句 都会执行,下面是__init__.py定义的示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# mypkg/__init__.py
print("包被导入了") # 会执行:打印语句
VERSION = "1.0" # 会执行:变量赋值
from .core import Engine # 会执行:触发 core.py 的完整执行
import logging # 会执行:导入依赖
logging.basicConfig(...) # 会执行:任何函数调用
def helper(): # 会执行 def 语句本身(创建函数对象)
print("hello") # 但函数体不执行,直到被调用
class Config: # 同理,class 语句执行(创建类对象)
def __init__(self):
DEBUG = True # 类体内的语句在创建类时执行一次- 关键区分:
def和class语句本身会执行(定义名字),但函数体只在调用时执行
- 关键区分:
导入的级联效应
- 如果
__init__.py里有from .core import Engine,那么导入包时会级联执行core.py的全部顶层代码,core.py的 import 又会继续级联 - 注:这是很多副作用(如注册机制、monkey patch)的实现基础
不同导入形式的差异
一般来说有下面几种导入方式:
1
2
3
4import mypkg # 只执行 mypkg/__init__.py
import mypkg.sub # 执行__init__.py + sub.py(父包一定先执行)
from mypkg import sub # 同上
from mypkg.sub import func # 同上,再把 func 绑定到当前命名空间核心说明:
- 导入子模块必然先执行父包的
__init__.py import mypkg不会自动导入子模块,除非__init__.py里显式导入了它们- 无论哪种形式, 模块只执行一次,之后都走
sys.modules缓存
- 导入子模块必然先执行父包的
常见问题陷阱
- 副作用不可控 :
__init__.py里的耗时操作(连数据库、加载模型)会拖慢所有导入者 - 循环导入 :A 导入 B,B 又导入 A
- 此时 A 还没执行完,B 拿到的是”半成品”模块,可能
AttributeError - 解决办法:延迟导入(在函数内 import)或重构依赖
- 此时 A 还没执行完,B 拿到的是”半成品”模块,可能
- 重复执行的错觉 :同一模块通过不同路径导入(如
import a和import pkg.a)可能在sys.modules中变成两个条目,导致代码执行两次- 注:sys.modules 是按照包名(路径)来作为 dict 管理包对象的
补充:__init__.py 中 __all__ 的特殊含义
__all__ 作用:硬性控制 from pkg import *
示例:
1
2
3
4
5# mypkg/__init__.py
from .core import Engine,_internal_helper
from .utils import format_data
__all__ = ["Engine", "format_data"]当用户执行
from mypkg import *时:- 定义了
__all__:只导入列表中列出的名字(Engine、format_data),且严格按列表来- 即使名字带下划线也会导入,列表中不存在的名字会报
AttributeError
- 即使名字带下划线也会导入,列表中不存在的名字会报
- 未定义
__all__:导入模块命名空间中所有不以下划线开头的名字- 注意:是所有 不以下划线开头 的,包括不小心引入的
import os中的os!
- 注意:是所有 不以下划线开头 的,包括不小心引入的
- 定义了
作为 “公共 API 契约” 的软性作用
- 即使没人用
import *,__all__仍然重要:- 文档意义 :明确声明”这些是包的公共接口,其他都是内部实现,随时可能变动”
- IDE / 补全 :许多编辑器和语言服务器用
__all__决定补全提示和 re-export 的合法性 - 类型检查器 :mypy 等工具在 strict 模式下,
from .core import Engine这种重导出如果不在__all__中(或不写成from .core import Engine as Engine),会被视为私有,外部使用时报错 - 文档生成工具 :Sphinx 等按
__all__决定哪些内容进入 API 文档
补充:__all__ 不能做什么
- 不是访问控制 :
__all__ = ["Engine"]并不阻止用户写mypkg._internal_helper,Python 没有真正的私有 - 不影响普通导入 :
from mypkg import xxx中的xxx不需要在__all__里 - 不影响执行 :
__all__只是一个普通的字符串列表赋值,不改变__init__.py的执行行为
__all__ 惯用写法
__init__.py写法示例:1
2
3
4
5
6
7
8
9
10
11# 方式一:显式列表(最常见)
__all__ = ["Engine", "Config", "run"]
# 方式二:重导出并聚合子模块的__all__
from .core import *
from .utils import *
from . import core, utils
__all__ = core.__all__ + utils.__all__
# 方式三:类型检查友好的显式重导出
from .core import Engine as Engine # "as 自身" 表示有意的公开重导出