Python 模組與套件 – module, package in Python
在開始前,會需要先具備Scope的基本觀念:Python變數範圍 – Scope
Module
module只是物件,來自特定的python資料型別types.ModuleType
import做了什麼事?
Python會直接從系統找尋已經存在的cache,若找不到此模組,才會繼續以下操作:
1. 建構一個ModuleType
物件
2. 將此物件動態(in run time)載入記憶體
3. 將此物件的關聯被加到global namespace,可透過globals()
訪問
4. 將物件的關聯加到系統cache,可透過sys.modules
訪問
5. 編譯及執行該模組的source code
用一個最直接的例子來驗證caching(但這樣寫有大麻煩,請勿這樣用):
1 2 3 4 |
import sys sys.modules['test'] = lambda: 'Test module caching' import test # passing |
那以上這些動作可不可以自己來呢?可以!步驟大致上長這樣:
除此之外,也可以透過importlib.import_module
來載入
這些模組從哪裡來?
- built-in
- 硬碟上的特定檔案,可透過
__file__
訪問
模組可以來自一般(.py)、預先編譯(.pyc)、壓縮檔(.zip)等…幾乎是沒什麼限制只要finders能找得到
python怎麼找到特定模組?——finders和loaders
Python:「Finder一號,你知道這個module在哪裡嗎?」
Finder一號: 「拍謝,我不知道ㄟ」
Python:「Finder二號,你知道這個module在哪裡嗎?」
Finder二號:「哦哦哦!我知道!來,我給你一張地圖,上面有說去哪找,也有說> 明找到後要交給Loader五號,他會幫你處理滴。」
Python:「Loader五號大大,請你幫我處理我手中這個module。」
Loader五號:「沒問題!」
上面這個對話裡的地圖,就是ModuleSpec
(可以透過importlib.util.find_spec
來取得)
1 2 |
ModuleSpec(name='fractions', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7fd4d0290c88>, origin='/usr/lib/python3.5/fractions.py') |
透過sys.meta_path
可以看到目前所有的Finders
1 2 3 4 |
[_frozen_importlib.BuiltinImporter, _frozen_importlib.FrozenImporter, _frozen_importlib_external.PathFinder,...] |
所以客製化自定義的finder跟loader是完全可行的,或是自訂一個importer處理所有操作,可以參考Site-specific configuration hook
不同import的方式(variants)
from math import sqrt
- 使用cache或載入math(一樣載入整個math module),但math不加到global namespace
sqrt
(symbol)加到global namespace且reference至math.sqrt
from math import sqrt as r_sqrt
- 使用cache或載入math(一樣載入整個math module),但math不加到global namespace
r_sqrt
(symbol)加到global namespace且reference至math.sqrt
from math import *
- 使用cache或載入math(一樣載入整個math module),但math不加到global namespace
- 將math定義過所有的symbol和各自的reference加到global namespace
對module來說,以上這些變化,其實都是載入整個module,只差在namespace加的變數不同,所以並沒有部份載入這回事(載入package的行為就會不同,可以做到部份載入)
以上這些變化在效率上差別很小,只差在lookup的次數,如math.sqrt(2)
、sqrt(2)
,所以還是依照需求來決定怎麼寫,並非from
的寫法就一定比較好
值得注意的是,from import *
可能會導致錯誤,因為相同名稱的symbol會在namespace中依照先後順序互相覆蓋,舉例來說:
1 2 3 |
from cmath import * from math import * |
此外,有時會避免symbol撞名導致覆蓋,會選擇在local scope來載入,這樣做的載入效能會略差,lookup次數的效能差別也會更為顯著
1 2 3 4 |
def func(): from math import sqrt sqrt(2) |
__name__
、__main__
__name__
變數用來顯示當前模組的名稱;如果該模組是程式入口,呼叫__name__
的值時Python會回傳__main__
字串,因此我們可以藉此模擬一種情境,來判斷當下的模組是否為程式入口:
1 2 3 |
if __name__ == '__main__': # do something if called by command line... |
Package
套件即是模組,但是此模組是由多個模組或子套件封裝而成,也就是目錄(directory)的概念,套件的名稱為目錄名,套件的屬性__path__
為目錄的絕對路徑
Python透過__path__
、__file__
、__package__
這些屬性來找到、封裝及載入你要的程式碼:
套件的入口__init__.py
:
- 告訴Python:請把這個目錄當作一個套件
- 套件的
__file__
屬性即自身__init__.py
的絕對路徑
幾個套件namespaces的封裝方法:
__init__.py
- 將變數命名為私有(_)
__all__
1 2 3 4 5 6 7 8 9 |
# pack1 from .mod1 import * from .mod2 import * __all__ = ( mod1.__all__ + mod2.__all__ ) |
Namespace package
與一般的套件相比,Namespace package不透過__init__.py
封裝(沒有__file__
屬性),其中的子套件可以來自不同的目錄位置