6. 模組 (Module)

如果從 Python 直譯器離開後又再次進入,之前(幫函式或變數)做的定義都會消失。因此,想要寫一些比較長的程式時,你最好使用編輯器來準備要輸入給直譯器的內容,並且用該檔案來運行它。這就是一個腳本 (script)。隨著你的程式越變越長,你可能會想要把它分開成幾個檔案,讓它比較好維護。你可能也會想用一個你之前已經在其他程式寫好的函式,但不想要複製該函式的原始定義到所有使用它的程式裡。

為了支援這一點,Python 有一種方法可以將定義放入檔案中,並在互動模式下的直譯器中使用它們。這種檔案稱為模組 (module);模組中的定義可以被 import 到其他模組中,或是被 import主 (main) 模組(在最頂層執行的腳本,以及互動模式下,所使用的變數集合)。

模組是指包含 Python 定義和語句的檔案,檔案名稱是模組名稱加上 .py。在模組中,模組的名稱(作為字串)會是全域變數 __name__ 的值。例如,用您喜歡的文字編輯器在資料夾中創一個名為 fibo.py 的檔案,內容如下:

# Fibonacci numbers module

def fib(n):    # write Fibonacci series up to n
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

def fib2(n):   # return Fibonacci series up to n
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

現在進入 Python 直譯器並用以下指令 import 這個模組:

>>> import fibo

這並不會將 fibo 中定義的函式名稱直接加入當前的 namespace 中(詳情請見 Python 作用域 (Scope) 及命名空間 (Namespace));它只會加入 fibo 的模組名稱。使用此模組名稱,就可以存取函式:

>>> fibo.fib(1000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
>>> fibo.fib2(100)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
>>> fibo.__name__
'fibo'

如果您打算經常使用其中某個函式,可以將其指定至區域變數:

>>> fib = fibo.fib
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

6.1. 深入了解模組

模組可以包含可執行的陳述式以及函式的定義。這些陳述式是作為模組的初始化,它們只會在第一次被 import 時才會執行。[1](如果檔案被當成腳本執行,也會執行它們)。

每個模組都有它自己的私有命名空間 (namespace),模組內定義的函式會把該模組的私有符號表當成全域命名空間使用。因此,模組的作者可以在模組中使用全域變數,而不必擔心和使用者的全域變數發生意外的名稱衝突。另一方面,如果你知道自己在做什麼,你可以用這個方式取用模組的全域變數,以和引用函式一樣的寫法,modname.itemname

在一個模組中可以 import 其他模組。把所有的 import 陳述式放在模組(就這邊來說,腳本也是一樣)的最開頭是個慣例,但並沒有強制。如放置在模組的最高層(不在任何函式或 class 中),被 import 的模組名稱將被加入全域命名空間中。

import 陳述式有另一種變形寫法,可以直接將名稱從欲 import 的模組,直接 import 至原模組的命名空間中。例如:

>>> from fibo import fib, fib2
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

在 import 之後的名稱會被導入,但定義該函式的整個模組名稱並不會被引入在區域命名空間中(因此,示例中的 fibo 未被定義)。

甚至還有另一種變形寫法,可以 import 模組定義的所有名稱:

>>> from fibo import *
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

這個寫法會 import 模組中所有的名稱,除了使用底線(_)開頭的名稱。大多數情況下,Python 程式設計師不大使用這個功能,因為它在直譯器中引入了一組未知的名稱,並且可能覆蓋了某些您已經定義的內容。

請注意,一般情況下並不建議從模組或套件中 import * 的做法,因為它通常會導致可讀性較差的程式碼。但若是使用它來在互動模式中節省打字時間,則是可以接受的。

如果模組名稱後面出現 as,則 as 之後的名稱將直接和被 import 模組綁定在一起。

>>> import fibo as fib
>>> fib.fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

這個 import 方式和 import fibo 實質上是一樣的,唯一的差別是現在要用 fib 使用模組。

在使用 from 時也可以用同樣的方式獲得類似的效果:

>>> from fibo import fib as fibonacci
>>> fibonacci(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

備註

出於效率原因,每個模組在每個直譯器 session 中僅會被 import 一次。因此,如果您更改了模組,則必須重啟直譯器——或者,如果只是一個想要在互動模式下測試的模組,可以使用 importlib.reload()。例如:import importlib; importlib.reload(modulename)

6.1.1. 把模組當作腳本執行

當使用以下內容運行 Python 模組時:

python fibo.py <arguments>

如同使用 import 指令,模組中的程式碼會被執行,但 __name__ 被設為 "__main__"。這意味著,透過在模組的末尾添加以下程式碼:

if __name__ == "__main__":
    import sys
    fib(int(sys.argv[1]))

你可以將檔案作為腳本也同時可以作為被 import 的模組,因為剖析 (parse) 命令列的程式碼只會在當模組是「主」檔案時,才會執行:

$ python fibo.py 50
0 1 1 2 3 5 8 13 21 34

如果此模組是被 import 的,則該段程式碼不會被執行:

>>> import fibo
>>>

這通常是用來為模組提供方便的使用者介面,或者用於測試目的(執行測試套件時,以腳本的方式執行模組)。

6.1.2. 模組的搜尋路徑

当导入一个名为 spam 的模块时,解释器首先会搜索具有该名称的内置模块。 这些模块的名称在 sys.builtin_module_names 中列出。 如果未找到,它将在变量 sys.path 所给出的目录列表中搜索名为 spam.py 的文件。 sys.path 是从这些位置初始化的:

  • 輸入腳本所位在的資料夾(如未指定檔案時,則是當前資料夾)。

  • PYTHONPATH(一連串和 shell 變數 PATH 的語法相同的資料夾名稱)。

  • 與安裝相關的預設值(按慣例會包含一個 site-packages 資料夾,它是由 site 模組所處理)。

sys.path 模块搜索路径的初始化 有更多的細節。

備註

在支援符號連結 (symlink) 的檔案系統中,輸入腳本的所在資料夾是在跟隨符號連結之後才被計算的。換言之,包含符號連結的資料夾並沒有增加到模組的搜尋路徑中。

初始化之後,Python 程式可以修改 sys.path。執行中腳本的所在資料夾會在搜尋路徑的開頭,在標準函式庫路徑之前。這代表該資料夾中的腳本會優先被載入,而不是函式庫資料夾中相同名稱的模組。除非是有意要做這樣的替換,否則這是一個錯誤。 請參見標準模組以瞭解更多資訊。

6.1.3. 「編譯」Python 檔案

為了加快載入模組的速度,Python 將每個模組的編譯版本暫存在 __pycache__ 資料夾下,並命名為 module.version.pyc, 這裡的 version 是編譯後的檔案的格式名稱,且名稱通常會包含 Python 的版本編號。例如,在 CPython 3.3 中,spam.py 的編譯版本將被暫存為 __pycache__/spam.cpython-33.pyc。此命名準則可以讓來自不同版本的編譯模組和 Python 的不同版本同時共存。

Python 根據原始碼最後修改的日期,檢查編譯版本是否過期而需要重新編譯。這是一個完全自動的過程。另外,編譯後的模組獨立於平台,因此不同架構的作業系統之間可以共用同一函式庫。

Python 在兩種情況下不檢查快取 (cache)。首先,它總是重新編譯且不儲存直接從命令列載入的模組的結果。第二,如果沒有源模組,則不會檢查快取。要支援非源模組(僅編譯)的發布,編譯後的模組必須位於原始資料夾中,並且不能有源模組。

一些給專家的秘訣:

  • 可以在 Python 指令上使用開關參數 (switch) -O-OO 來減小已編譯模組的大小。開關參數 -O 刪除 assert(斷言)陳述式,而 -OO 同時刪除 assert 陳述式和 __doc__ 字串。由於有些程式可能依賴於上述這些內容,因此只有在您知道自己在做什麼時,才應使用此參數。「已優化」模組有 opt- 標記,且通常較小。未來的版本可能會改變優化的效果。

  • 讀取 .pyc 檔案時,程式的執行速度並不會比讀取 .py 檔案快。唯一比較快的地方是載入的速度。

  • 模組 compileall 可以為資料夾中的所有模組創建 .pyc 檔。

  • 更多的細節,包括決策流程圖,請參考PEP 3147

6.2. 標準模組

Python 附帶了一個標準模組庫,詳細的介紹在另一份文件,稱為「Python 函式庫參考手冊」(簡稱為「函式庫參考手冊」)。有些模組是直譯器中內建的;它們使一些不屬於語言核心但依然內建的運算得以存取,其目的是為了提高效率,或提供作業系統基本操作(例如系統呼叫)。這些模組的集合是一個組態選項,它們取決於底層平台。例如:winreg 模組僅供 Windows 使用。值得注意的模組是 sys,它被內建在每個 Python 直譯器中。變數 sys.ps1sys.ps2 則用來定義主、次提示字元的字串:

>>> import sys
>>> sys.ps1
'>>> '
>>> sys.ps2
'... '
>>> sys.ps1 = 'C> '
C> print('Yuck!')
Yuck!
C>

只有直譯器在互動模式時,才需要定義這兩個變數。

變數 sys.path 是一個字串 list,它決定直譯器的模組搜尋路徑。它的初始值為環境變數 PYTHONPATH 中提取的預設路徑,或是當 PYTHONPATH 未設定時,從內建預設值提取。你可以用標準的 list 操作修改該變數:

>>> import sys
>>> sys.path.append('/ufs/guido/lib/python')

6.3. dir() 函式

內建函式 dir() 用於找出模組定義的所有名稱。它回傳一個排序後的字串 list:

>>> import fibo, sys
>>> dir(fibo)
['__name__', 'fib', 'fib2']
>>> dir(sys)  
['__breakpointhook__', '__displayhook__', '__doc__', '__excepthook__',
 '__interactivehook__', '__loader__', '__name__', '__package__', '__spec__',
 '__stderr__', '__stdin__', '__stdout__', '__unraisablehook__',
 '_clear_type_cache', '_current_frames', '_debugmallocstats', '_framework',
 '_getframe', '_git', '_home', '_xoptions', 'abiflags', 'addaudithook',
 'api_version', 'argv', 'audit', 'base_exec_prefix', 'base_prefix',
 'breakpointhook', 'builtin_module_names', 'byteorder', 'call_tracing',
 'callstats', 'copyright', 'displayhook', 'dont_write_bytecode', 'exc_info',
 'excepthook', 'exec_prefix', 'executable', 'exit', 'flags', 'float_info',
 'float_repr_style', 'get_asyncgen_hooks', 'get_coroutine_origin_tracking_depth',
 'getallocatedblocks', 'getdefaultencoding', 'getdlopenflags',
 'getfilesystemencodeerrors', 'getfilesystemencoding', 'getprofile',
 'getrecursionlimit', 'getrefcount', 'getsizeof', 'getswitchinterval',
 'gettrace', 'hash_info', 'hexversion', 'implementation', 'int_info',
 'intern', 'is_finalizing', 'last_traceback', 'last_type', 'last_value',
 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path', 'path_hooks',
 'path_importer_cache', 'platform', 'prefix', 'ps1', 'ps2', 'pycache_prefix',
 'set_asyncgen_hooks', 'set_coroutine_origin_tracking_depth', 'setdlopenflags',
 'setprofile', 'setrecursionlimit', 'setswitchinterval', 'settrace', 'stderr',
 'stdin', 'stdout', 'thread_info', 'unraisablehook', 'version', 'version_info',
 'warnoptions']

沒有給引數時,dir() 列出目前已定義的名稱:

>>> a = [1, 2, 3, 4, 5]
>>> import fibo
>>> fib = fibo.fib
>>> dir()
['__builtins__', '__name__', 'a', 'fib', 'fibo', 'sys']

請注意,它列出所有類型的名稱:變數、模組、函式等。

dir() 不會列出內建函式和變數的名稱。如果你想要列出它們,它們被定義在標準模組 builtins 內:

>>> import builtins
>>> dir(builtins)  
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning',
 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError',
 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning',
 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False',
 'FileExistsError', 'FileNotFoundError', 'FloatingPointError',
 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError',
 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError',
 'MemoryError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented',
 'NotImplementedError', 'OSError', 'OverflowError',
 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError',
 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning',
 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',
 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError',
 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError',
 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning',
 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__',
 '__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs',
 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable',
 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits',
 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit',
 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr',
 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass',
 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview',
 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property',
 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars',
 'zip']

6.4. 套件 (Package)

包是通过使用“带点号模块名”来构造 Python 模块命名空间的一种方式。 例如,模块名 A.B 表示名为 A 的包中名为 B 的子模块。 就像使用模块可以让不同模块的作者不必担心彼此的全局变量名一样,使用带点号模块名也可以让 NumPy 或 Pillow 等多模块包的作者也不必担心彼此的模块名冲突。

假設你想設計一個能統一處理音訊檔案及音訊數據的模組集(「套件」)。因為音訊檔案有很多的不同的格式(通常以它們的副檔名來辨識,例如:.wav.aiff.au),因此,為了不同檔案格式之間的轉換,你會需要建立和維護一個不斷增長的模組集合。為了要達成對音訊數據的許多不同作業(例如,音訊混合、增加回聲、套用等化器功能、創造人工立體音效),你將編寫一系列無止盡的模組來執行這些作業。以下是你的套件可能的架構(以階層式檔案系統的方式表示):

sound/                          Top-level package
      __init__.py               Initialize the sound package
      formats/                  Subpackage for file format conversions
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              ...
      effects/                  Subpackage for sound effects
              __init__.py
              echo.py
              surround.py
              reverse.py
              ...
      filters/                  Subpackage for filters
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py
              ...

Import 套件時,Python 會搜尋 sys.path 裡的目錄,尋找套件的子目錄。

需要有 __init__.py 文件才能让 Python 将包含该文件的目录当作包来处理(除非使用 namespace package,这是一个相对高级的特性)。 这可以防止重名的目录如 string 在无意中屏蔽后继出现在模块搜索路径中的有效模块。 在最简单的情况下,__init__.py 可以只是一个空文件,但它也可以执行包的初始化代码或设置 __all__ 变量,这将在稍后详细描述。

套件使用者可以從套件中 import 個別模組,例如:

import sound.effects.echo

这将加载子模块 sound.effects.echo。 它必须通过其全名来引用。

sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

另一種 import 子模組的方法是:

from sound.effects import echo

这也会加载子模块 echo,并使其不必加包前缀,因此可按如下方式使用:

echo.echofilter(input, output, delay=0.7, atten=4)

另一種變化是直接 import 所需的函式或變數:

from sound.effects.echo import echofilter

同样,这将加载子模块 echo,但这使其函数 echofilter() 直接可用:

echofilter(input, output, delay=0.7, atten=4)

請注意,使用 from package import item 時,item 可以是套件的子模組(或子套件),也可以是套件中被定義的名稱,像是函式、class (類別)或變數。import 陳述式首先測試套件中有沒有定義該 item;如果沒有,則會假設它是模組,並嘗試載入。如果還是找不到 item,則會引發 ImportError 例外。

相反地,使用 import item.subitem.subsubitem 語法時,除了最後一項之外,每一項都必須是套件;最後一項可以是模組或套件,但不能是前一項中定義的 class、函式或變數。

6.4.1. 從套件中 import *

當使用者寫下 from sound.effects import * 時,會發生什麼事?理想情況下,我們可能希望程式碼會去檔案系統,尋找套件中存在的子模組,並將它們全部 import。這會花費較長的時間,且 import 子模組的過程可能會有不必要的副作用,這些副作用只有在明確地 import 子模組時才會發生。

唯一的解法是由套件作者為套件提供明確的索引。import 陳述式使用以下慣例:如果套件的 __init__.py 程式碼有定義一個名為 __all__ 的 list,若遇到 from package import * 的時候,它就會是要被 import 的模組名稱。發布套件的新版本時,套件作者可自行決定是否更新此 list。如果套件作者認為沒有人會從他的套件中 import *,他也可能會決定不支援這個 list。舉例來說,sound/effects/__init__.py 檔案可包含以下程式碼:

__all__ = ["echo", "surround", "reverse"]

这意味着 from sound.effects import * 将导入 sound.effects 包的三个命名子模块。

请注意子模块可能会受到本地定义名称的影响。 例如,如果你在 sound/effects/__init__.py 文件中添加了一个 reverse 函数,from sound.effects import * 将只导入 echosurround 这两个子模块,但 不会 导入 reverse 子模块,因为它被本地定义的 reverse 函数所遮挡:

__all__ = [
    "echo",      # refers to the 'echo.py' file
    "surround",  # refers to the 'surround.py' file
    "reverse",   # !!! refers to the 'reverse' function now !!!
]

def reverse(msg: str):  # <-- this name shadows the 'reverse.py' submodule
    return msg[::-1]    #     in the case of a 'from sound.effects import *'

如果没有定义 __all__from sound.effects import * 语句 不会 把包 sound.effects 中的所有子模块都导入到当前命名空间;它只是确保包 sound.effects 已被导入(可能还会运行 __init__.py 中的任何初始化代码),然后再导入包中定义的任何名称。 这包括由 __init__.py 定义的任何名称(以及显式加载的子模块)。 它还包括先前 import 语句显式加载的包里的任何子模块。 请看以下代码:

import sound.effects.echo
import sound.effects.surround
from sound.effects import *

在本例中,echosurround 模块被导入到当前命名空间,因为在执行 from...import 语句时它们已在 sound.effects 包中定义了。 (当定义了 __all__ 时也是如此)。

雖然,有些特定模組的設計,讓你使用 import * 時,該模組只會輸出遵循特定樣式的名稱,但在正式環境 (production) 的程式碼中這仍然被視為是不良習慣。

記住,使用 from package import specific_submodule 不會有任何問題!實際上,這是推薦用法,除非 import 的模組需要用到的子模組和其他套件的子模組同名。

6.4.2. 套件內引用

当包由多个子包构成(如示例中的 sound 包)时,可以使用绝对导入来引用同级包的子模块。 例如,如果 sound.filters.vocoder 模块需要使用 sound.effects 包中的 echo 模块,它可以使用 from sound.effects import echo

你还可以编写相对导入代码,即使用 from module import name 形式的 import 语句。 这些导入使用前导点号来表示相对导入所涉及的当前包和上级包。 例如对于 surround 模块,可以使用:

from . import echo
from .. import formats
from ..filters import equalizer

請注意,相對 import 的運作是以目前的模組名稱為依據。因為主模組的名稱永遠是 "__main__",所以如果一個模組預期被用作 Python 應用程式的主模組,那它必須永遠使用絕對 import。

6.4.3. 多目錄中的套件

套件也支援一個特殊屬性 __path__。它在初始化時是一個 list,包含該套件的 __init__.py 檔案所在的目錄名稱,初始化時機是在這個檔案的程式碼被執行之前。這個變數可以被修改,但這樣做會影響將來對套件內的模組和子套件的搜尋。

雖然這個特色不太常被需要,但它可用於擴充套件中的模組集合。

註解