Python 中的黑暗角落(三):模組與包
如果你用過 Python,那麼你一定用過 import
關鍵字載入過各式各樣的模組。但你是否熟悉 Python 中的模組與包的概念呢?或者,以下幾個問題,你是否有明確的答案?
- 什麼是模組?什麼又是包?
from matplotlib.ticker import Formatter, FixedLocator
中的matplotlib
和ticker
分別是什麼?中間的句點是什麼意思?from matplotlib.pyplot import *
中,import *
的背後會發生什麼?
魯迅先生說:「於無聲處聽驚雷」,講的是平淡時卻有令人驚奇、意外的事情。import
相關的模組、包的概念也是如此。如果你對上面幾個問題存有疑問,那麼這篇就是為你而作的。
模組
為什麼要有模組
眾所周知,Python 有一個互動式的直譯器。在直譯器中,你可以使用 Python 的所有功能。但是,直譯器是一次性的。也就是說,如果你關掉直譯器,那麼先前定義、執行的一切東西,都會丟失不見。另一方面,在直譯器中輸入程式碼是一件很麻煩的事情;這是因為在直譯器中複用程式碼比較困難。
為此,人們會把相對穩定、篇幅較長的程式碼儲存在一個純文字檔案中。一般來說,我們把這樣副檔名為 .py
的檔案稱為 Python 指令碼。為了提高程式碼複用率,我們可以把一組相關的
Python 相關的定義、宣告儲存在同一個 .py
檔案中。此時,這個 Python 指令碼就是一個 Python 模組(Module)。我們可以在直譯器中,或者在其他
Python 指令碼中,通過 import
模組的識別
和 Python 中的其它物件一樣,Python 也為模組定義了一些形如 __foo__
的變數。對於模組來說,最重要的就是它的名字 __name__
了。每當
Python 執行指令碼,它就會為該指令碼賦予一個名字。對於「主程式」來說,這一指令碼的 __name__
被定義為 "__main__"
;對於被 import
進主程式的模組來說,這一指令碼的 __name__
被定義為指令碼的檔名(base
filename)。因此,我們可以用 if __name__ == "__main__":
在模組程式碼中定義一些測試程式碼。
1 |
def fib_yield(n): a, b = 0, 1 while b < n: yield b a, b = b, a+b def fib(n): for num in fib_yield(n): print(num) if __name__ == "__main__": fib(10) |
我們將其儲存為 fibonacci.py
,而後在 Python 直譯器中 import
它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
In [1]: import fibonacci In [2]: dir(fibonacci) Out[2]: ['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'fib', 'fib_yield'] In [3]: print(fibonacci.__name__) fibonacci In [4]: fibonacci.fib(5) 1 1 2 3 In [5]: for num in fibonacci.fib_yield(5): ...: print(num) ...: 1 1 2 3 |
可以觀察到,fibonacci.py
在作為模組引入時,fibonacci.__name__
被設定為檔名 "fibonacci"
。但若在命令列直接執行 python
fibonacci.py
,則 if
語句塊會被執行,此時 __name__
是 "__main__"
。
模組的內部變數和初始化
Python 為每個模組維護了單獨的符號表,因此可以實現類似 C++ 中名字空間(namespace)的功能。Python 模組中的函式,可以使用模組的內部變數,完成相關的初始化操作;同時,import
模組的時候,也不用擔心這些模組內部變數與使用者自定義的變數同名衝突。
1 2 3 4 5 6 7 |
foo = 0 def show(): print(foo) if __name__ == "__main__": show() |
此處我們在模組 module_var
內部定義了內部變數 foo
,並且在函式 show
中引用了它。
1 2 3 4 5 6 7 8 9 10 11 12 |
In [7]: import module_var ...: ...: foo = 3 ...: ...: print(foo) ...: print(module_var.foo) ...: ...: module_var.show() ...: 3 0 0 |
值得一提的是,模組的初始化操作(這裡指 foo = 0
這條語句),僅只在 Python 直譯器第一次處理該模組的時候執行。也就是說,如果同一個模組被多次 import
,它只會執行一次初始化。
from ... import ...
模組提供了類似名字空間的限制,不過 Python 也允許從模組中匯入指定的符號(變數、函式、類等)到當前模組。匯入後,這些符號就可以直接使用,而不需要字首模組名。
1 2 3 4 5 6 7 8 9 |
In [8]: from fibonacci import fib_yield, fib In [9]: fib(10) 1 1 2 3 5 8 |
值得一提的是,被匯入的符號,如果引用了模組內部的變數,那麼在匯入之後也依然會使用模組內的變數,而不是當前環境中的同名變數。
1 2 3 4 5 6 |
In [11]: from module_var import show In [12]: foo = 3 In [13]: show() 0 |
也有更粗暴的方式,匯入模組內的所有公開符號(沒有字首 _
的那些)。不過,一般來說,除了實驗、排查,不建議這樣做。因為,通常你不知道模組定義了哪些符號、是否與當前環境有重名的符號。一旦有重名,那麼,這樣粗暴地匯入模組內所有符號,就會覆蓋掉當前環境的版本。從而造成難以排查的錯誤。
模組搜尋路徑
之前我們都在討論模組的好處,但是忽略了一個問題:Python 怎樣知道從何處找到模組檔案?
如果你熟悉命令列,那麼這個問題對你來說就不難理解。在命令列中執行的任何命令,實際上背後都對應了一個可執行檔案。命令列直譯器(比如 cmd, bash)會從一個全域性的環境變數 PATH
中讀取一個有序的列表。這個列表包含了一系列的路徑,而命令列直譯器,會依次在這些路徑裡,搜尋需要的可執行檔案。
Python 搜尋模組檔案,也遵循了類似的思路。比如,使用者在 Python 中嘗試匯入 import foobar
,那麼
- 首先,Python 會在內建模組中搜尋
foobar
; - 若未找到,則 Python 會在當前工作路徑(當前指令碼所在路徑,或者執行 Python 直譯器的路徑)中搜尋
foobar
; - 若仍未找到,則 Python 會在環境變數
PYTHONPATH
中指示的路徑中搜尋foobar
; - 若依舊未能找到,則 Python 會在安裝時指定的路徑中搜尋
foobar
; - 若仍舊失敗,則 Python 會報錯,提示找不到
foobar
這個模組。
1 2 3 4 5 6 7 |
In [14]: import foobar --------------------------------------------------------------------------- ImportError Traceback (most recent call last) <ipython-input-14-909badd622c0> in <module>() ----> 1 import foobar ImportError: No module named foobar |
pyc
檔案
和 LaTeX 中遇到的問題一樣:裝載大量文字檔案是很慢的。因此 Python 也採用了類似 LaTeX 的解決方案:將模組編譯成容易裝載的檔案,並儲存起來(相當於 LaTeX 中的 dump 格式檔案 .fmt
)。這些編譯好並儲存起來的檔案,有後綴名 .pyc
。
當 Python 編譯好模組之後,下次載入時,Python 就會讀取相應的 .pyc
檔案,而不是 .py
檔案。而裝載 .pyc
檔案會比裝載 .py
檔案更快。
值得一提的是,對於 .pyc
,很多人一直有誤解。事實上,從執行的角度,裝載 .pyc
並不比裝載 .py
檔案更快。此處的加速,僅只在裝載模組的過程中起作用。因此 .pyc
中的 C
更多地可以理解為
cache。
包
包(package)是 Python 中對模組的更高一級的抽象。簡單來說,Python 允許使用者把目錄當成模組看待。這樣一來,目錄中的不同模組檔案,就變成了「包」裡面的子模組。此外,包目錄下還可以有子目錄,這些子目錄也可以是 Python 包。這種分層,對模組識別、管理,都是非常有好處的。特別地,對於一些大型 Python 工具包,內裡可能有成百上千個不同功能的模組。若是逐個模組釋出,那簡直成了災難。
科學計算領域,SciPy, NumPy, Matplotlib 等第三方工具,都是用包的形式釋出的。
目錄結構
Python 要求每一個「包」目錄下,都必須有一個名為 __init__.py
的檔案。從這個檔案的名字上看,首先它有 __
作為前後綴,我們就知道,這個檔案肯定是
Python 內部用來做某種識別用的;其次,它有 init
,我們知道它一定和初始化有關;最後,它有 .py
作為字尾名,因此它也是一個
Python 模組,可以完成一些特定的工作。
現在假設你想編寫一個 Python 工具包,用來處理圖片,它可能由多個 Python 模組組成。於是你會考慮把它做成一個 Python 包,內部按照功能分成若干子包,再繼續往下分成不同模組去實現。比如會有這樣的目錄結構。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
picture/ Top-level package __init__.py Initialize the picture package formats/ Subpackage for file format conversions __init__.py jpgread.py jpgwrite.py pngread.py pngwrite.py bmpread.py bmpwrite.py ... filters/ Subpackage for filters __init__.py boxblur.py gaussblur.py sharpen.py ... |
此處 picture
目錄下有 __init__.py
,因此
Python 會將其作為一個 Python 包;類似地,子目錄 formats
和 filters
就成了 picture
下的子包。這裡,子包的劃分以功能為準。formats
下的模組,設計用於處理不同格式的圖片檔案的讀寫;而 filters
下的模組,則被設計用於實現各種濾鏡效果。
使用 Python 包
Python 包的使用和模組的使用類似,是很自然的方式。以我們的 picture
包為例,若你想使用其中具體的模組,可以這樣做。
1
|
import picutre.filters.gaussblur
|
如此,你就匯入了 picture
包中 filters
子包中的 gaussblur
模組,你就能使用高斯模糊模組提供的功能了。具體使用方式,和使用模組也保持一致。
1
|
picture.filters.gaussblur.gaussblur_filter(input, output)
|
這看起來很繁瑣,因此你可能會喜歡用 from ... import ...
語句,脫去過多的名字限制。
1
|
from picture.filters import gaussblur
|
這樣一來,你就可以直接按如下方式使用高斯模糊這一濾鏡了。
1
|
gaussblur.gaussblur_filter(input, output)
|
__init__.py
之前簡單地介紹了 __init__.py
這個特殊的檔案,但未展開。這裡我們展開詳說。
首先的問題是,為什麼要設計 __init__.py
,而不是自動地把任何一個目錄都當成是 Python 包?這主要是為了防止重名造成的問題。比如,很可能使用者在目錄下新建了一個子目錄,名為 collections
;但
Python 有內建的同名模組。若不加任何限制地,將子目錄當做是 Python 包,那麼,import collections
就會引入這個 Python 包。而這樣的行為,可能不是使用者預期的。從這個意義上說,設計 __init__.py
是一種保護措施。
接下來的問題是,__init__.py
具體還有什麼用?
首先來說,__init__.py
可以執行一些初始化的操作。這是因為,__init__.py
作為模組檔案,會在相應的
Python 包被引入時首先引入。這就是說,import picture
相當於是 import
picture.__init__
。因此,__init__.py
中可以保留一些初始化的程式碼——比如引入依賴的其他 Python 模組。
其次,細心的你可能發現,上一小節中,我們沒有介紹對 Python 包的 from picture import *
的用法。這是因為,從一個包中匯入所有內容,這一行為是不明確的;必須要由包的作者指定。我們可以在 __init__.py
中定義名為 __all__
的
Python 列表。這樣一來,就能使用 from picture import *
了。
具體來說,我們可以在 picture/__init__.py
中做如下定義。
1 2 3 |
import collections # import the built-in package __all__ = ["formats", "filters"] |
此時,若我們在使用者模組中 from picture import *
,則首先會引入 Python 內建的 collections
模組,而後引入 picture.formats
和 picture.filters
這兩個
Python 子包了。
在包內使用相對層級引用其他模組
細心的你應該已經發現,在引入 Python 包中的模組時,我們用句點 .
代替了斜線(或者反斜線)來標記路徑的層級(實際上是包和模組的層級)。在 Python 包的內部,我們也可以使用類似相對路徑的方式,使用相對層級來簡化包內模組的互相引用。
比如,在 gaussblur.py
中,你可以通過以下四種方式,引入 boxblur.py
,而它們的效果是一樣的。
1 2 3 4 |
import boxblur from . import boxblur from ..filters import boxblur from .. import filters.boxblur as boxblur |