跳转至

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__ 构造方法)做了铺垫。