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.py 是以 import 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")

在套件內部,子模組彼此之間既可以用完整的帶點名稱、也可以用一個以開頭的點代表「這個套件」的 相對 import 來互相取用:

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))

相對 import 能讓套件保持自我包含:重新命名套件目錄並不需要去編輯每一個子模組。

當單一檔案大到超過了舒適的尺寸,或當一組相關模組理應一同歸屬於同一個命名空間之下時,就使用套件。對於日常的指令碼,單一一個 .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'

用這個模式為某個函式庫檔案附上一個快速的冒煙測試或示範,而不會干擾到引入它的指令碼。