6. 模塊?

退出 Python 解釋器后,再次進(jìn)入時,之前在 Python 解釋器中定義的函數(shù)和變量就丟失了。因此,編寫較長程序時,建議用文本編輯器代替解釋器,執(zhí)行文件中的輸入內(nèi)容,這就是編寫 腳本 。隨著程序越來越長,為了方便維護(hù),最好把腳本拆分成多個文件。編寫腳本還一個好處,不同程序調(diào)用同一個函數(shù)時,不用每次把函數(shù)復(fù)制到各個程序。

為實現(xiàn)這些需求,Python 把各種定義存入一個文件,在腳本或解釋器的交互式實例中使用。這個文件就是 模塊 ;模塊中的定義可以 導(dǎo)入 到其他模塊或 模塊(在頂層和計算器模式下,執(zhí)行腳本中可訪問的變量集)。

模塊是包含 Python 定義和語句的文件。其文件名是模塊名加后綴名 .py 。在模塊內(nèi)部,通過全局變量 __name__ 可以獲取模塊名(即字符串)。例如,用文本編輯器在當(dāng)前目錄下創(chuàng)建 fibo.py 文件,輸入以下內(nèi)容:

# 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

現(xiàn)在,進(jìn)入 Python 解釋器,用以下命令導(dǎo)入該模塊:

>>>
>>> import fibo

這項操作不直接把 fibo 函數(shù)定義的名稱導(dǎo)入到當(dāng)前符號表,只導(dǎo)入模塊名 fibo 。要使用模塊名訪問函數(shù):

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

如果經(jīng)常使用某個函數(shù),可以把它賦值給局部變量:

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

6.1. 模塊詳解?

模塊包含可執(zhí)行語句及函數(shù)定義。這些語句用于初始化模塊,且僅在 import 語句 第一次 遇到模塊名時執(zhí)行。1 (文件作為腳本運行時,也會執(zhí)行這些語句。)

模塊有自己的私有符號表,用作模塊中所有函數(shù)的全局符號表。因此,在模塊內(nèi)使用全局變量時,不用擔(dān)心與用戶定義的全局變量發(fā)生沖突。另一方面,可以用與訪問模塊函數(shù)一樣的標(biāo)記法,訪問模塊的全局變量,modname.itemname。

可以把其他模塊導(dǎo)入模塊。按慣例,所有 import 語句都放在模塊(或腳本)開頭,但這不是必須的。導(dǎo)入的模塊名存在導(dǎo)入模塊的全局符號表里。

import 語句有一個變體,可以直接把模塊里的名稱導(dǎo)入到另一個模塊的符號表。例如:

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

這段代碼不會把模塊名導(dǎo)入到局部符號表里(因此,本例沒有定義 fibo)。

還有一種變體可以導(dǎo)入模塊內(nèi)定義的所有名稱:

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

這種方式會導(dǎo)入所有不以下劃線(_)開頭的名稱。大多數(shù)情況下,不要用這個功能,這種方式向解釋器導(dǎo)入了一批未知的名稱,可能會覆蓋已經(jīng)定義的名稱。

注意,一般情況下,不建議從模塊或包內(nèi)導(dǎo)入 *, 因為,這項操作經(jīng)常讓代碼變得難以理解。不過,為了在交互式編譯器中少打幾個字,這么用也沒問題。

模塊名后使用 as 時,直接把 as 后的名稱與導(dǎo)入模塊綁定。

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

import fibo 一樣,這種方式也可以有效地導(dǎo)入模塊,唯一的區(qū)別是,導(dǎo)入的名稱是 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

備注

為了保證運行效率,每次解釋器會話只導(dǎo)入一次模塊。如果更改了模塊內(nèi)容,必須重啟解釋器;僅交互測試一個模塊時,也可以使用 importlib.reload(),例如 import importlib; importlib.reload(modulename)。

6.1.1. 以腳本方式執(zhí)行模塊?

可以用以下方式運行 Python 模塊:

python fibo.py <arguments>

這項操作將執(zhí)行模塊里的代碼,和導(dǎo)入模塊一樣,但會把 __name__ 賦值為 "__main__"。 也就是把下列代碼添加到模塊末尾:

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

既可以把這個文件當(dāng)腳本使用,也可以用作導(dǎo)入的模塊, 因為,解析命令行的代碼只有在模塊以 “main” 文件執(zhí)行時才會運行:

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

導(dǎo)入模塊時,不運行這些代碼:

>>>
>>> import fibo
>>>

這種操作常用于為模塊提供便捷用戶接口,或用于測試(把模塊當(dāng)作執(zhí)行測試套件的腳本運行)。

6.1.2. 模塊搜索路徑?

When a module named spam is imported, the interpreter first searches for a built-in module with that name. These module names are listed in sys.builtin_module_names. If not found, it then searches for a file named spam.py in a list of directories given by the variable sys.path. sys.path is initialized from these locations:

  • 輸入腳本的目錄(或未指定文件時的當(dāng)前目錄)。

  • PYTHONPATH (目錄列表,與 shell 變量 PATH 的語法一樣)。

  • The installation-dependent default (by convention including a site-packages directory, handled by the site module).

More details are at The initialization of the sys.path module search path.

備注

在支持 symlink 的文件系統(tǒng)中,輸入腳本目錄是在追加 symlink 后計算出來的。換句話說,包含 symlink 的目錄并 沒有 添加至模塊搜索路徑。

初始化后,Python 程序可以更改 sys.path。運行腳本的目錄在標(biāo)準(zhǔn)庫路徑之前,置于搜索路徑的開頭。即,加載的是該目錄里的腳本,而不是標(biāo)準(zhǔn)庫的同名模塊。 除非刻意替換,否則會報錯。詳見 標(biāo)準(zhǔn)模塊

6.1.3. “已編譯的” Python 文件?

為了快速加載模塊,Python 把模塊的編譯版緩存在 __pycache__ 目錄中,文件名為 module.version.pyc,version 對編譯文件格式進(jìn)行編碼,一般是 Python 的版本號。例如,CPython 的 3.3 發(fā)行版中,spam.py 的編譯版本緩存為 __pycache__/spam.cpython-33.pyc。使用這種命名慣例,可以讓不同 Python 發(fā)行版及不同版本的已編譯模塊共存。

Python 對比編譯版本與源碼的修改日期,查看它是否已過期,是否要重新編譯,此過程完全自動化。此外,編譯模塊與平臺無關(guān),因此,可在不同架構(gòu)系統(tǒng)之間共享相同的支持庫。

Python 在兩種情況下不檢查緩存。其一,從命令行直接載入模塊,只重新編譯,不存儲編譯結(jié)果;其二,沒有源模塊,就不會檢查緩存。為了支持無源文件(僅編譯)發(fā)行版本, 編譯模塊必須在源目錄下,并且絕不能有源模塊。

給專業(yè)人士的一些小建議:

  • 在 Python 命令中使用 -O-OO 開關(guān),可以減小編譯模塊的大小。-O 去除斷言語句,-OO 去除斷言語句和 __doc__ 字符串。有些程序可能依賴于這些內(nèi)容,因此,沒有十足的把握,不要使用這兩個選項?!皟?yōu)化過的”模塊帶有 opt- 標(biāo)簽,并且文件通常會一小些。將來的發(fā)行版或許會改進(jìn)優(yōu)化的效果。

  • .pyc 文件讀取的程序不比從 .py 讀取的執(zhí)行速度快,.pyc 文件只是加載速度更快。

  • compileall 模塊可以為一個目錄下的所有模塊創(chuàng)建 .pyc 文件。

  • 本過程的細(xì)節(jié)及決策流程圖,詳見 PEP 3147。

6.2. 標(biāo)準(zhǔn)模塊?

Python 自帶一個標(biāo)準(zhǔn)模塊的庫,它在 Python 庫參考(此處以下稱為"庫參考" )里另外描述。 一些模塊是內(nèi)嵌到編譯器里面的, 它們給一些雖并非語言核心但卻內(nèi)嵌的操作提供接口,要么是為了效率,要么是給操作系統(tǒng)基礎(chǔ)操作例如系統(tǒng)調(diào)入提供接口。 這些模塊集是一個配置選項, 并且還依賴于底層的操作系統(tǒng)。 例如,winreg 模塊只在 Windows 系統(tǒng)上提供。一個特別值得注意的模塊 sys,它被內(nèi)嵌到每一個 Python 編譯器中。sys.ps1sys.ps2 變量定義了一些字符,它們可以用作主提示符和輔助提示符:

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

只有解釋器用于交互模式時,才定義這兩個變量。

變量 sys.path 是字符串列表,用于確定解釋器的模塊搜索路徑。該變量以環(huán)境變量 PYTHONPATH 提取的默認(rèn)路徑進(jìn)行初始化,如未設(shè)置 PYTHONPATH,則使用內(nèi)置的默認(rèn)路徑??梢杂脴?biāo)準(zhǔn)列表操作修改該變量:

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

6.3. dir() 函數(shù)?

內(nèi)置函數(shù) dir() 用于查找模塊定義的名稱。返回結(jié)果是經(jīng)過排序的字符串列表:

>>>
>>> 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']

沒有參數(shù)時,dir() 列出當(dāng)前定義的名稱:

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

注意,該函數(shù)列出所有類型的名稱:變量、模塊、函數(shù)等。

dir() 不會列出內(nèi)置函數(shù)和變量的名稱。這些內(nèi)容的定義在標(biāo)準(zhǔn)模塊 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. ?

包是一種用“點式模塊名”構(gòu)造 Python 模塊命名空間的方法。例如,模塊名 A.B 表示包 A 中名為 B 的子模塊。正如模塊可以區(qū)分不同模塊之間的全局變量名稱一樣,點式模塊名可以區(qū)分 NumPy 或 Pillow 等不同多模塊包之間的模塊名稱。

假設(shè)要為統(tǒng)一處理聲音文件與聲音數(shù)據(jù)設(shè)計一個模塊集(“包”)。聲音文件的格式很多(通常以擴展名來識別,例如:.wav.aiff, .au),因此,為了不同文件格式之間的轉(zhuǎn)換,需要創(chuàng)建和維護(hù)一個不斷增長的模塊集合。為了實現(xiàn)對聲音數(shù)據(jù)的不同處理(例如,混聲、添加回聲、均衡器功能、創(chuàng)造人工立體聲效果),還要編寫無窮無盡的模塊流。下面這個分級文件樹展示了這個包的架構(gòu):

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

導(dǎo)入包時,Python 搜索 sys.path 里的目錄,查找包的子目錄。

Python 只把含 __init__.py 文件的目錄當(dāng)成包。這樣可以防止以 string 等通用名稱命名的目錄,無意中屏蔽出現(xiàn)在后方模塊搜索路徑中的有效模塊。 最簡情況下,__init__.py 只是一個空文件,但該文件也可以執(zhí)行包的初始化代碼,或設(shè)置 __all__ 變量,詳見下文。

還可以從包中導(dǎo)入單個模塊,例如:

import sound.effects.echo

這段代碼加載子模塊 sound.effects.echo ,但引用時必須使用子模塊的全名:

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

另一種導(dǎo)入子模塊的方法是 :

from sound.effects import echo

這段代碼還可以加載子模塊 echo ,不加包前綴也可以使用。因此,可以按如下方式使用:

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

Import 語句的另一種變體是直接導(dǎo)入所需的函數(shù)或變量:

from sound.effects.echo import echofilter

同樣,這樣也會加載子模塊 echo,但可以直接使用函數(shù) echofilter()

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

注意,使用 from package import item 時,item 可以是包的子模塊(或子包),也可以是包中定義的函數(shù)、類或變量等其他名稱。import 語句首先測試包中是否定義了 item;如果未在包中定義,則假定 item 是模塊,并嘗試加載。如果找不到 item,則觸發(fā) ImportError 異常。

相反,使用 import item.subitem.subsubitem 句法時,除最后一項外,每個 item 都必須是包;最后一項可以是模塊或包,但不能是上一項中定義的類、函數(shù)或變量。

6.4.1. 從包中導(dǎo)入 *?

使用 from sound.effects import * 時會發(fā)生什么?理想情況下,該語句在文件系統(tǒng)查找并導(dǎo)入包的所有子模塊。這項操作花費的時間較長,并且導(dǎo)入子模塊可能會產(chǎn)生不必要的副作用,這種副作用只有在顯式導(dǎo)入子模塊時才會發(fā)生。

唯一的解決方案是提供包的顯式索引。import 語句使用如下慣例:如果包的 __init__.py 代碼定義了列表 __all__,運行 from package import * 時,它就是用于導(dǎo)入的模塊名列表。發(fā)布包的新版本時,包的作者應(yīng)更新此列表。如果包的作者認(rèn)為沒有必要在包中執(zhí)行導(dǎo)入 * 操作,也可以不提供此列表。例如,sound/effects/__init__.py 文件包含以下代碼:

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

This would mean that from sound.effects import * would import the three named submodules of the sound.effects package.

如果沒有定義 __all__,from sound.effects import * 語句 不會 把包 sound.effects 中所有子模塊都導(dǎo)入到當(dāng)前命名空間;該語句只確保導(dǎo)入包 sound.effects (可能還會運行 __init__.py 中的初始化代碼),然后,再導(dǎo)入包中定義的名稱。這些名稱包括 __init__.py 中定義的任何名稱(以及顯式加載的子模塊),還包括之前 import 語句顯式加載的包里的子模塊。請看以下代碼:

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

本例中,執(zhí)行 from...import 語句時,將把 echosurround 模塊導(dǎo)入至當(dāng)前命名空間,因為,它們是在 sound.effects 包里定義的。(該導(dǎo)入操作在定義了 __all__ 時也有效。)

雖然,可以把模塊設(shè)計為用 import * 時只導(dǎo)出遵循指定模式的名稱,但仍不提倡在生產(chǎn)代碼中使用這種做法。

記住,使用 from package import specific_submodule 沒有任何問題! 實際上,除了導(dǎo)入模塊使用不同包的同名子模塊之外,這種方式是推薦用法。

6.4.2. 子包參考?

包中含有多個子包時(與示例中的 sound 包一樣),可以使用絕對導(dǎo)入引用兄弟包中的子模塊。例如,要在模塊 sound.filters.vocoder 中使用 sound.effects 包的 echo 模塊時,可以用 from sound.effects import echo 導(dǎo)入。

還可以用 import 語句的 from module import name 形式執(zhí)行相對導(dǎo)入。這些導(dǎo)入語句使用前導(dǎo)句點表示相對導(dǎo)入中的當(dāng)前包和父包。例如,相對于 surround 模塊,可以使用:

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

注意,相對導(dǎo)入基于當(dāng)前模塊名。因為主模塊名是 "__main__" ,所以 Python 程序的主模塊必須始終使用絕對導(dǎo)入。

6.4.3. 多目錄中的包?

包還支持特殊屬性 __path__。該屬性初始化為在包的 __init__.py 文件中的代碼執(zhí)行前所在的目錄名列表。這個變量可以修改,但這樣做會影響將來搜索包中模塊和子包的操作。

這個功能雖然不常用,但可用于擴展包中的模塊集。

備注

1

實際上,函數(shù)定義也是“可執(zhí)行”的“語句”;執(zhí)行模塊級函數(shù)定義時,函數(shù)名將被導(dǎo)入到模塊的全局符號表。