2.19. 编写模块

任何 .py 文件都是一个模块。把一个不断增长的脚本拆分到几个文件中,能让每个文件保持简短,并让通用的辅助代码在脚本之间共享。

2.19.1. 拆分一个脚本

把一组相关的函数提取到它们自己的文件中:

camera_utils.py:

def banner():
    print("OpenMV")

def label(text):
    return "[" + text + "]"

main.py:

import camera_utils

camera_utils.banner()
print(camera_utils.label("ready"))

输出:

OpenMV
[ready]

这两个文件并排放在同一个目录里。当 main.py 运行时,import camera_utils 会读取一次 camera_utils.py,执行它的顶层语句,并把得到的模块对象绑定到 main 中的名称 camera_utils 上。从其他任何地方再来一次 import camera_utils 都会返回同一个对象 —— 模块在首次加载后会被缓存。

模块的名称来自它的文件名,所以 camera_utils.pyimport camera_utils 的形式被导入。

2.19.2. 多文件模块(包)

模块也可以是一个文件的 目录,而不是单个 .py。该目录的名称成为模块名,里面的文件则是它的 子模块

camera_utils/
    __init__.py
    text.py
    timing.py

__init__.py 是包本身被导入时运行的文件;它可以是空的,也可以从子模块中重新导出选定的名称。子模块通过点号分隔的名称来访问:

import camera_utils.text
from camera_utils.timing import elapsed

camera_utils.text.label("ready")

在包内部,子模块之间既可以通过完整的点号名称相互访问,也可以通过以一个前导点表示“本包”的 相对导入 来访问:

camera_utils/timing.py:

from . import text             # the sibling submodule

def stamp(value):
    return text.label(str(value))

若要改为引入一个特定的名称,就在点号分隔的同级模块名后面写出它:

from .text import label

def stamp(value):
    return label(str(value))

相对导入让一个包保持自包含:重命名包目录无需编辑每一个子模块。

当单个文件增长超过一个舒适的大小时,或者当一组相关模块应当归属于同一个命名空间下时,就使用包。对于日常脚本,单个 .py 文件就足够了。

2.19.3. __name__ 守卫

每个模块都有一个内置名称 __name__。它的值取决于文件是如何被使用的:

  • 当文件被 直接运行 时,__name__ 被设为字符串 "__main__"

  • 当文件被另一个脚本 导入 时,__name__ 被设为模块名 —— 即不含 .py 的文件名。

利用这一点的惯用写法是:

label_util.py:

def label(text):
    return "[" + text + "]"

if __name__ == "__main__":
    print(label("self-test"))

输出(直接运行时):

[self-test]

当同一个文件改为被 导入 时,__name__ 被设为模块名,所以 if 块会被跳过,不会有额外的东西运行:

>>> import label_util
>>> label_util.__name__
'label_util'

用这个模式给一个库文件附加一个快速的冒烟测试或演示,而不会扰乱那些导入它的脚本。