Python教程-6模組

m0_38012394發表於2019-02-01

6. 模組

如果你退出 Python 直譯器並重新進入,你做的任何定義(變數和方法)都會丟失。因此,如果你想要編寫一些更大的程式,為準備直譯器輸入使用一個文字編輯器會更好,並以那個檔案替代作為輸入執行。這就是傳說中的 指令碼。隨著你的程式變得越來越長,你可能想要將它分割成幾個更易於維護的檔案。你也可能想在不同的程式中使用順手的函式,而不是把程式碼在它們之間中拷來拷去。

為了滿足這些需要,Python 提供了一個方法可以從檔案中獲取定義,在指令碼或者直譯器的一個互動式例項中使用。這樣的檔案被稱為 模組;模組中的定義可以 匯入 到另一個模組或 主模組 中(在指令碼執行時可以呼叫的變數集位於最高階,並且處於計算器模式)。

模組是包括 Python 定義和宣告的檔案。檔名就是模組名加上.py字尾。模組的模組名(做為一個字串)可以由全域性變數__name__ 得到。例如,你可以用自己慣用的檔案編輯器在當前目錄下建立一個叫 fibo.py 的檔案,錄入如下內容:

# Fibonacci numbers module

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

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

現在進入 Python 直譯器並使用以下命令匯入這個模組:

>>> import fibo

這樣做不會直接把fibo中的函式匯入當前的語義表;它只是引入了模組名 fibo。你可以通過模組名按如下方式訪問這個函式:

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

如果打算頻繁使用一個函式,你可以將它賦予一個本地變數:

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

6.1. 深入模組

除了包含函式定義外,模組也可以包含可執行語句。這些語句一般用來初始化模組。他們僅在 第一次 被匯入的地方執行一次。[1]

每個模組都有自己私有的符號表,被模組內所有的函式定義作為全域性符號表使用。因此,模組的作者可以在模組內部使用全域性變數,而無需擔心它與某個使用者的全域性變數意外衝突。從另一個方面講,如果你確切的知道自己在做什麼,你可以使用引用模組函式的表示法訪問模組的全域性變數,modname.itemname

模組可以匯入其他的模組。一個(好的)習慣是將所有的 import 語句放在模組的開始(或者是指令碼),這並非強制。被匯入的模組名會放入當前模組的全域性符號表中。

import 語句的一個變體直接從被匯入的模組中匯入命名到本模組的語義表中。例如:

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

這樣不會從局域語義表中匯入模組名(如上所示, fibo 沒有定義)。

甚至有種方式可以匯入模組中的所有定義:

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

這樣可以匯入所有除了以下劃線(_)開頭的命名。

需要注意的是在實踐中往往不鼓勵從一個模組或包中使用 *匯入所有,因為這樣會讓程式碼變得很難讀。不過,在互動式會話中這樣用很方便省力。

Note

出於效能考慮,每個模組在每個直譯器會話中只匯入一遍。因此,如果你修改了你的模組,需要重啟直譯器;或者,如果你就是想互動式的測試這麼一個模組,可以用 imp.reload() 重新載入,例如 import imp; imp.reload(modulename)

6.1.1. 作為指令碼來執行模組

當你使用以下方式執行 Python 模組時,模組中的程式碼便會被執行:
python fibo.py <arguments>

模組中的程式碼會被執行,就像匯入它一樣,不過此時 __name__被設定為 "__main__"。這相當於,如果你在模組後加入如下程式碼:

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

就可以讓此檔案像作為模組匯入時一樣作為指令碼執行。此程式碼只有在模組作為 “main” 檔案執行時才被呼叫:

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

如果模組被匯入,不會執行這段程式碼:

>>> import fibo
>>>

這通常用來為模組提供一個便於測試的使用者介面(將模組作為指令碼執行測試需求)。

6.1.2. 模組的搜尋路徑
匯入一個叫spam的模組時,直譯器先在當前目錄中搜尋名為 spam.py 的檔案。如果沒有找到的話,接著會到 sys.path 變數中給出的目錄列表中查詢。 sys.path 變數的初始值來自如下:

  • 輸入指令碼的目錄(當前目錄)。

  • 環境變數 PYTHONPATH 表示的目錄列表中搜尋
    (這和 shell 變數 PATH具有一樣的語法,即一系列目錄名的列表)。

  • Python 預設安裝路徑中搜尋。

Note

在支援符號連線的檔案系統中,輸入的指令碼所在的目錄是符號連線指向的目錄。 換句話說也就是包含符號連結的目錄不會被加到目錄搜尋路徑中

實際上,直譯器由 sys.path 變數指定的路徑目錄搜尋模組,該變數初始化時預設包含了輸入指令碼(或者當前目錄), PYTHONPATH 和安裝目錄。這樣就允許 Python 程式瞭解如何修改或替換模組搜尋目錄。需要注意的是由於這些目錄中包含有搜尋路徑中執行的指令碼,所以這些指令碼不應該和標準模組重名,否則在匯入模組時 Python 會嘗試把這些指令碼當作模組來載入。這通常會引發錯誤。請參見 標準模組 以瞭解更多的資訊。

6.1.3. “編譯的” Python 檔案

為了加快載入模組的速度,Python 會在 __pycache__目錄下以 module.version.pyc名字快取每個模組編譯後的版本,這裡的版本編制了編譯後檔案的格式。它通常會包含 Python 的版本號。例如,在 CPython 3.3 版中,spam.py 編譯後的版本將快取為 __pycache__/spam.cpython-33.pyc。這種命名約定允許由不同釋出和不同版本的 Python 編譯的模組同時存在。

Python 會檢查原始檔與編譯版的修改日期以確定它是否過期並需要重新編譯。這是完全自動化的過程。同時,編譯後的模組是跨平臺的,所以同一個庫可以在不同架構的系統之間共享。

Python 不檢查在兩個不同環境中的快取。首先,它會永遠重新編譯而且不會儲存直接從命令列載入的模組。其次,如果沒有源模組它不會檢查快取。若要支援沒有原始檔(只有編譯版)的釋出,編譯後的模組必須在源目錄下,並且必須沒有原始檔的模組。

部分高階技巧:

  • 為了減少一個編譯模組的大小,你可以在 Python 命令列中使用 -O 或者 -OO-O 引數刪除了斷言語句,-OO引數刪除了斷言語句和 doc 字串。

    因為某些程式依賴於這些變數的可用性,你應該只在確定無誤的場合使用這一選項。“優化的” 模組有一個 .pyo 字尾而不是 .pyc字尾。未來的版本可能會改變優化的效果。

  • 來自 .pyc檔案或 .pyo檔案中的程式不會比來自 .py檔案的執行更快;.pyc.pyo檔案只是在它們載入的時候更快一些。

  • compileall 模組可以為指定目錄中的所有模組建立 .pyc檔案(或者使用 -O 引數建立 .pyo 檔案)。

  • 在 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 是直譯器模組搜尋路徑的字串列表。它由環境變數 PYTHONPATH 初始化,如果沒有設定 PYTHONPATH ,就由內建的預設值初始化。你可以用標準的字串操作修改它:

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

6.3. dir() 函式

內建函式 dir() 用於按模組名搜尋模組定義,它返回一個字串型別的儲存列表:

>>> import fibo, sys
>>> dir(fibo)
['__name__', 'fib', 'fib2']
>>> dir(sys)  
['__displayhook__', '__doc__', '__excepthook__', '__loader__', '__name__',
 '__package__', '__stderr__', '__stdin__', '__stdout__',
 '_clear_type_cache', '_current_frames', '_debugmallocstats', '_getframe',
 '_home', '_mercurial', '_xoptions', 'abiflags', 'api_version', 'argv',
 'base_exec_prefix', 'base_prefix', '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',
 'getcheckinterval', 'getdefaultencoding', 'getdlopenflags',
 'getfilesystemencoding', 'getobjects', 'getprofile', 'getrecursionlimit',
 'getrefcount', 'getsizeof', 'getswitchinterval', 'gettotalrefcount',
 'gettrace', 'hash_info', 'hexversion', 'implementation', 'int_info',
 'intern', 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path',
 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'ps1',
 'setcheckinterval', 'setdlopenflags', 'setprofile', 'setrecursionlimit',
 'setswitchinterval', 'settrace', 'stderr', 'stdin', 'stdout',
 'thread_info', 'version', 'version_info', 'warnoptions']

無引數呼叫時,dir() 函式返回當前定義的命名:

>>> a = [1, 2, 3, 4, 5]
>>> import fibo
>>> fib = fibo.fib
>>> dir()
['__builtins__', '__doc__', '__file__', '__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. 包

包通常是使用用“圓點模組名”的結構化模組名稱空間。例如,名為 A.B 的模組表示了名為 A 的包中名為B 的子模組。正如同用模組來儲存不同的模組架構可以避免全域性變數之間的相互衝突,使用圓點模組名儲存像 NumPy 或 Python Imaging Library 之類的不同類庫架構可以避免模組之間的命名衝突。

假設你現在想要設計一個模組集(一個“包”)來統一處理聲音檔案和聲音資料。存在幾種不同的聲音格式(通常由它們的副檔名來標識,例如:.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
              ...

當匯入這個包時,Python 通過 sys.path 搜尋路徑查詢包含這個包的子目錄。

為了讓 Python 將目錄當做內容包,目錄中必須包含 __init__.py 檔案。這是為了避免一個含有爛俗名字的目錄無意中隱藏了稍後在模組搜尋路徑中出現的有效模組,比如 string。最簡單的情況下,只需要一個空的 __init__.py檔案即可。當然它也可以執行包的初始化程式碼,或者定義稍後介紹的 __all__變數。

使用者可以每次只匯入包裡的特定模組,例如:
import sound.effects.echo

這樣就匯入了 sound.effects.echo 子模組。它必需通過完整的名稱來引用:

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

匯入包時有一個可以選擇的方式:

from sound.effects import echo

這樣就載入了 echo 子模組,並且使得它在沒有包字首的情況下也可以使用,所以它可以如下方式呼叫:

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

還有另一種變體用於直接匯入函式或變數:

from sound.effects.echo import echofilter

這樣就又一次載入了 echo子模組,但這樣就可以直接呼叫它的 echofilter() 函式:
echofilter(input, output, delay=0.7, atten=4)

需要注意的是使用 from package import item 方式匯入包時,這個子項(item)既可以是包中的一個子模組(或一個子包),也可以是包中定義的其它命名,像函式、類或變數。import 語句首先核對是否包中有這個子項,如果沒有,它假定這是一個模組,並嘗試載入它。如果沒有找到它,會引發一個 ImportError 異常。

相反,使用類似 import item.subitem.subsubitem 這樣的語法時,這些子項必須是包,最後的子項可以是包或模組,但不能是前面子項中定義的類、函式或變數。

6.4.1. 從 * 匯入包
那麼當使用者寫下 from sound.effects import *時會發生什麼事?理想中,總是希望在檔案系統中找出包中所有的子模組,然後匯入它們。這可能會花掉很長時間,並且出現期待之外的邊界效應,匯出了希望只能顯式匯入的包。

對於包的作者來說唯一的解決方案就是給提供一個明確的包索引。import 語句按如下條件進行轉換:執行 from package import * 時,如果包中的 __init__.py程式碼定義了一個名為 __all__的列表,就會按照列表中給出的模組名進行匯入。新版本的包釋出時作者可以任意更新這個列表。如果包作者不想 import * 的時候匯入他們的包中所有模組,那麼也可能會決定不支援它( import * )。例如, sound/effects/__init__.py 這個檔案可能包括如下程式碼:

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

這意味著 from sound.effects import *語句會從 sound 包中匯入以上三個已命名的子模組。

如果沒有定義 __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 * 時它只匯出符合某種規範/模式的命名,仍然不建議在生產程式碼中使用這種寫法。

記住,from Package import specific_submodule 沒有錯誤!事實上,除非匯入的模組需要使用其它包中的同名子模組,否則這是推薦的寫法。

6.4.2. 包內引用
如果包中使用了子包結構(就像示例中的 sound 包),可以按絕對位置從相鄰的包中引入子模組。例如,如果 sound.filters.vocoder 包需要使用 sound.effects 包中的 echo 模組,它可以 from sound.Effects import echo

你可以用這樣的形式 from module import name 來寫顯式的相對位置匯入。那些顯式相對匯入用點號標明關聯匯入當前和上級包。以 surround 模組為例,你可以這樣用:

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

需要注意的是顯式或隱式相對位置匯入都基於當前模組的命名。因為主模組的名字總是 "__main__",Python 應用程式的主模組應該總是用絕對匯入。

6.4.3. 多重目錄中的包

包支援一個更為特殊的特性, path。 在包的 __init__.py檔案程式碼執行之前,該變數初始化一個目錄名列表。該變數可以修改,它作用於包中的子包和模組的搜尋功能。

這個功能可以用於擴充套件包中的模組集,不過它不常用。

Footnotes

[1] 事實上函式定義既是“宣告”又是“可執行體”;執行體由函式在模組全域性語義表中的命名匯入。

相關文章