- 示例工程
- 教程
【免费下载链接】python-mastery
Advanced Python Mastery (course by @dabeaz)
本篇技术指南以《Advanced Python Mastery》课程第 9 章第一讲(Exercises/ex9_1.md)为核心,系统讲解 Python 模块的基础机制:模块如何被首次加载、sys.path与sys.modules在加载流程中的作用、重复导入为何不会重新执行模块代码、from module import的"名称引用"语义,以及importlib.reload()带来的类分裂等隐蔽陷阱。读完本文,你将不仅掌握模块导入的完整底层行为,还能在调试与库开发中避开 reload 相关的一系列危险用法。
本练习是课程第 9 章"Modules and Packages"(模块与包)的起点,后续练习 Exercises/ex9_2.md(把多个模块组装成structly包)与 Exercises/ex9_3.md(__all__符号控制与模块拆分)都建立在本讲的模块基础之上。课程要求 Python 3.6 及以上版本,本仓库环境实测为 Python 3.12.10。
第一步:创建第一个库模块 simplemod.py
在仓库顶层目录(与Exercises/、Data/同级)新建一个极简的库模块文件simplemod.py,内容如下:
# simplemod.py x = 42 # A global variable # A simple function def foo(): print('x is', x) # A simple class class Spam: def yow(self): print('Yow!') # A scripting statement print('Loaded simplemod')这个模块刻意同时包含四类内容:
- 一个全局变量
x = 42; - 一个函数
foo(),它在调用时读取模块全局变量x; - 一个类
Spam,带方法yow(); - 一条顶层脚本语句
print('Loaded simplemod')——它会在模块加载时立刻执行,成为后续观察模块加载行为的"探针"。
仓库中已提供与本练习完全一致的参考实现 Solutions/9_1/simplemod.py(其中foo()使用"x is %s" % x的格式化写法,效果等价)。对照本教程自行创建的版本即可。
(a) 模块加载与系统路径(sys.path)
直接在交互式解释器(REPL)中导入刚创建的模块:
>>> import simplemod Loaded simplemod >>> simplemod.foo() x is 42 >>>Loaded simplemod这行输出说明:import 语句会执行模块文件中的全部顶层代码——变量赋值、函数/类定义、顶层print均按顺序运行。
如果导入失败并抛出ImportError,说明你的路径设置有问题。Python 通过sys.path决定到哪里寻找模块,可用如下方式查看:
>>> import sys >>> sys.path ... look at the result ... >>>sys.path是一个由字符串组成的搜索路径列表,典型内容依次包括:
- 当前脚本所在目录(交互模式下为当前工作目录,即空字符串
''); - 由环境变量
PYTHONPATH追加的目录; - Python 标准库目录;
- 第三方包目录(如
site-packages)。
因此,只要simplemod.py位于你启动 Python 时的当前工作目录下,导入就会成功;否则就需要调整PYTHONPATH或把文件放到sys.path中的目录里。
(b) 重复加载:模块只执行一次
再次导入同一模块,注意观察:
>>> import simplemod >>>没有任何输出——Loaded simplemod不再出现。这说明模块只被加载(执行)一次,之后的import直接从缓存中取出已加载的模块对象,不会重新执行文件内容。
验证"模块是单例"的更直观方法是修改其全局变量后再次导入:
>>> simplemod.x 42 >>> simplemod.x = 13 >>> simplemod.x 13 >>> import simplemod >>> simplemod.x 13 >>>把x改成13后再次import simplemod,x依然是13——导入操作没有重置模块状态。
强制重新加载:importlib.reload()
如果确实需要强制模块重新加载(例如开发调试时改了代码),使用importlib.reload():
>>> import importlib >>> importlib.reload(simplemod) Loaded simplemod <module 'simplemod' from 'simplemod.py'> >>> simplemod.x 42 >>>reload()会重新执行模块文件,x因此恢复为42,模块对象的内存地址(<module ...>后的十六进制地址)也可能变化——这一点在本讲的 (d) 部分会引发严重后果。
sys.modules:已加载模块的注册表
Python 用sys.modules这个字典记录所有已加载模块,键是模块名(如'simplemod'),值是模块对象。查看并操作它:
>>> sys.modules ... look at output ... >>> sys.modules['simplemod'] <module 'simplemod' from 'simplemod.py'> >>> del sys.modules['simplemod'] >>> import simplemod Loaded simplemod >>>从sys.modules中删除'simplemod'条目后,再次导入就会重新执行模块文件(Loaded simplemod再次出现)。由此可以理解import的完整流程:
- 检查
sys.modules中是否存在同名模块——存在则直接返回,不做任何文件读取; - 不存在则按
sys.path顺序查找模块文件; - 找到后创建模块对象并执行文件内容,同时把模块注册进
sys.modules; - 执行完成后返回模块对象。
(c) from module import:名称引用而非复制
重启 Python(清空已加载模块状态)后,试一下只导入选定的符号:
>>> ############### [ RESTART ] ############### >>> from simplemod import foo Loaded simplemod >>> foo() x is 42 >>>注意两点:
Loaded simplemod说明from ... import ...同样会完整加载并执行整个模块文件,并非只解析被导入的那一个名字;foo()内部读取的x是模块全局变量x,所以输出x is 42。
但此时模块对象本身不可见:
>>> simplemod.foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'simplemod' is not defined >>>from simplemod import foo只把foo这个名称绑定到当前命名空间,simplemod这个名字并没有被绑定,因此直接引用会抛NameError。
关键语义:导入的是"名称引用"而不是值的拷贝
>>> from simplemod import x, foo >>> x 42 >>> foo() x is 42 >>> x = 13 >>> foo() x is 42 # !! Please explain >>> x 13 >>>这段输出值得仔细解释:
from simplemod import x把当前命名空间中的名字x绑定到模块里x当前指向的整数对象42;- 之后执行
x = 13,只是让当前命名空间的x改指向新对象13,并没有修改模块内部的x; - 而
foo()是模块内定义的函数,它读取的是模块命名空间里的x(仍为42),因此输出依旧是x is 42。
结论:from module import name建立的只是指向模块内对象的"别名/引用",对导入名称的重新赋值不会反向影响模块内部状态。这也是为什么 Python 社区对from module import *持谨慎态度——导入的符号与模块后续状态可能脱节(详见后续练习 Exercises/ex9_3.md 对__all__的讨论)。
(d) 破坏性的 reload():两份代码同时存在
创建Spam的实例:
>>> import simplemod >>> s = simplemod.Spam() >>> s.yow() Yow! >>>现在不要重启 Python,直接编辑simplemod.py,把Spam.yow()的实现改为:
# simplemod.py ... class Spam: def yow(self): print('More Yow!')然后执行 reload,观察新旧对象的行为差异:
>>> importlib.reload(simplemod) Loaded simplemod <module 'simplemod' from 'simplemod.py'> >>> s.yow() 'Yow!' >>> t = simplemod.Spam() >>> t.yow() 'More Yow!' >>>诡异的事情发生了:s和t都是Spam的实例,但调用的yow()却是两个不同版本的实现——s输出旧的'Yow!',t输出新的'More Yow!'。是的,新旧两版代码在同一进程内同时存在。
原因在于reload()只是重新执行了模块文件并原地更新模块对象的命名空间(让simplemod.Spam指向新的类对象),但它不会去更新那些已经创建出来的旧实例。实例s的__class__仍指向旧的类对象,因此继续使用旧方法。
这种"新旧混杂"还会带来更多怪异行为,比如isinstance检查也会失效:
>>> s <simplemod.Spam object at 0x1006940b8> >>> isinstance(s, simplemod.Spam) False >>> isinstance(t, simplemod.Spam) True >>>isinstance(s, simplemod.Spam)返回False——因为s属于旧的Spam类,而simplemod.Spam已经指向 reload 后创建的新类,两者不是同一个类对象。只有 reload 之后创建的t才与新的simplemod.Spam匹配。
结论与最佳实践
底线:不要依赖 reload 来处理任何重要的事情。reload 在以下场景可能还算可用:
- 交互式调试时快速验证对模块代码的改动;
- 前提是你清楚它的局限性和危险性,并且不会保留跨 reload 的旧实例、旧类引用。
在正式的程序逻辑、长期运行的服务、或需要对象状态一致性的场景中,重启解释器才是正确的做法。本练习揭示的这些行为,正是 Python 模块系统"加载一次、缓存复用"设计的直接推论——理解sys.modules、sys.path、名称绑定与 reload 的边界,是后续把模块组装成包(Exercises/ex9_2.md)和理解包内符号导出控制(Exercises/ex9_3.md)的前提。
延伸阅读与练习对照
- 完整练习题面:Exercises/ex9_1.md;官方参考答案说明见 Exercises/soln9_1.md(本题答案即"按练习步骤操作即可")。
- 本练习的参考模块文件:Solutions/9_1/simplemod.py。
- 课程练习总索引:Exercises/index.md;配套讲义为仓库根目录的 PythonMastery.pdf。
- 下一讲:Exercises/ex9_2.md 将把这套模块知识落地,把此前各练习积累的
structure.py、validate.py、reader.py、tableformat.py组织成structly包。
- 示例工程
- 教程
【免费下载链接】python-mastery
Advanced Python Mastery (course by @dabeaz)
相关推荐
Advanced Python Mastery 练习指南:python-mastery 仓库九大模块的学习路径与实战地图
Advanced Python Mastery 练习指南:python mastery 仓库九大模块的学习路径与实战地图 本指南以 Exercises/inde
示例工程教程Python Mastery 实战解析:模块定义与 import 语句——以 Exercise 1.6 为核心深入理解 Python 模块机制
Python Mastery 实战解析:模块定义与 import 语句——以 Exercise 1.6 为核心深入理解 Python 模块机制 本篇技术指南以
示例工程教程Python 一等公民与内存模型实战:从通用 CSV 解析器到列式存储(Advanced Python Mastery 练习 2.6 深度解析)
Python 一等公民与内存模型实战:从通用 CSV 解析器到列式存储(Advanced Python Mastery 练习 2.6 深度解析) 本指南以 Ad
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考