9.3 模块与包——包¶
内容梗概¶
本课讲解"包"(package)的概念与用法:包是一组模块的集合,即一个包含多个 .py 文件的文件夹。老师以 sound/formats 的多层目录为例,演示了 __init__.py 的作用与包导入时初始化文件逐级执行的顺序;讲解了为什么不推荐 from 包 import *,以及必须配合在 __init__.py 中定义 __all__ 列表才能使用星号导入;最后讲解包/子包内部模块之间要用相对路径(from . import xxx、from .. import xxx)互相导入。本课偏理论、无作业,老师鼓励大家在后续实验中按包的结构组织代码。
知识点详解¶
包的概念(约 00:00–00:56)¶
- 包 = 一组模块的集合 = 一个文件夹:把相关功能的
.py文件放进一个文件夹提供给他人使用,就形成了一个包。 - 模块是文件,包是由这些文件组成的文件夹,但它是特殊的文件夹。
- 演示目录结构:在当前目录建子文件夹
sound,里面再建子包formats,其中放置wavread.py、wavwrite.py等模块(模块内容可以为空,结构对即可)。
__init__.py(约 00:27–01:24)¶
- 双下划线在 Python 中是保留符号的标记(联系模块中的
__name__等变量)。 - 文件夹下有了
__init__.py才能被(规范地)当作一个包导入;它的作用是写包的基本信息(如author = "安静")和初始化代码。 - 老师课堂上自我纠正:没有
__init__.py时文件夹有时也能作为包导入,"只是不太合规",建议一定要有。
包的导入与逐级初始化(约 01:24–02:45)¶
- 包和模块的导入方式一致,可用之前讲的多种导入形式:导入整个包、导入包中某个子包(如
import sound.formats)、或导入子包中某个具体模块。 - 逐级初始化:导入
sound.formats时,Python 会先执行外层sound/__init__.py,再执行formats/__init__.py——按层级逐级运行各层的初始化文件(通过在各__init__.py中写print可观察到执行顺序)。
不推荐 from 包 import * 与 __all__(约 02:45–11:07)¶
- 不推荐
from sound import *这类直接导入包内所有内容的写法:包里可能有很多子包/子模块,全部导入会导致加载速度和效率很差。 - 更进一步:对包使用星号导入时,直接调用子模块中的函数会报
NameError: name 'xxx' is not defined——**除非**在该包的__init__.py中定义了__all__变量(双下划线)。 - 演示:在
formats/__init__.py中写__all__ = ['wavread', 'wavwrite'](列出星号导入时要导入的模块名)后,from sound.formats import *才能正常导入这两个模块,进而调用wavread.read()等函数(空函数pass时打印结果为None)。 - 一般仍不推荐这么用,但读别人代码时会经常见到,需要理解。
子包中使用相对路径互相导入(约 11:36–14:31)¶
- 场景:在
wavread.py里想调用同目录的wavwrite.py,或调用上一级其他子包的模块。 - 直接写
import sound.xxx是不行的(找不到主包入口),要用**相对路径导入**: from . import wavwrite—— 导入当前文件夹(当前包)下的模块;from .. import effects或from ..effects import echo——..表示退到上一层(如从formats退到sound),再进入兄弟子包导入其中模块。- 与之前讲过的文件相对路径概念(
.当前目录、..上级目录)一致。
本课小结(约 14:31–15:37)¶
- 包和模块结合紧密,新知识点有两个:
__init__.py的用法与执行顺序——导入时按层级逐级加载;- 子包内模块相互调用要用相对路径。
- 本节偏理论、没有实验、没有作业;希望大家在后面的实验中尽量按包的结构组织自己的代码。
示例与演示¶
- 搭建包结构:在实验目录下创建
sound/主包、sound/formats/子包,放入wavread.py、wavwrite.py等模块(内容可为空函数pass)。 - 初始化顺序演示:在各层
__init__.py中写打印语句(如定义author变量、打印初始化信息),import sound.formats后观察到先执行sound/__init__.py再执行formats/__init__.py。 from sound.formats import *演示:未定义__all__时调用模块报name is not defined;在formats/__init__.py中加入__all__ = ['wavread', 'wavwrite']后导入成功,调用wavread.read()(空函数)输出None。- 细节观察:导入时即使未调用
wavwrite,只要它列在__all__中也会被自动编译,生成.pyc字节码缓存(呼应 9.2 的__pycache__)。 - 相对导入演示:在
wavread.py中用from . import wavwrite导入隔壁模块;用from .. import effects(退一层再进effects子包)导入echo模块。
重点与难点¶
__init__.py:包规范上必须有;导入子包时各层__init__.py会**按层级依次执行**,自己做实验时要注意这一点。from 包 import *不推荐;要用必须在__init__.py里定义__all__列表,否则星号导入后无法访问子模块(报not defined)。- 包内模块互相引用必须用相对路径(
.、..),直接import 主包.子模块在包内部不可行——这是易错点。 - 理论性内容在小程序里用不上,但看别人的中大型项目代码时很常见,且后续实验要求按此结构组织代码。
关联内容¶
- 上接 9.1(模块概念、四种导入方式、
__name__等内置变量)与 9.2(sys.path搜索路径、__pycache__字节码缓存,本课导入包时同样生成了.pyc)。 - 相对路径的
./..记法呼应此前文件操作章节中的路径概念。 - 下接第 10 章"第三方库与虚拟环境":第三方库本质上就是别人写好的包,
pip install后装入site-packages。 - 双下划线命名(
__init__、__all__、__name__)为后续第 11–12 章类的特殊方法(如__init__构造方法)做了铺垫。