最近為了在 Mapleboard 上架設一個仿 Google Cloud Function (GCF) 的輕量級 Serverless 執行平台, 我詢問 AI 解決方案時發現可以用 Python 內建模組 importlib 來實現, 我之前都沒用過, 所以就先來測試一下此模組用法.
importlib 為 Python 標準函式庫中的內建模組, 用來動態載入 (dynamic import) 其他模組, 或是在程式執行時重新載入已經載入過的模組, 它提供了比一般 import 更靈活的功能, 特別適合在執行時根據字串名稱動態導入模組或呼叫其函式 (需要導入的模組名稱事先未知), 常見於 Plugin 系統, Serverless 執行平台, 與模組熱更新等應用場景.
首先匯入 importlib 模組 :
>>> import importlib
用 dir() 檢視 importlib 模組內容 :
>>> dir(importlib)
['_RELOADING', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__import__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '_abc', '_bootstrap', '_bootstrap_external', '_imp', '_pack_uint32', '_unpack_uint32', 'find_loader', 'import_module', 'invalidate_caches', 'machinery', 'reload', 'sys', 'util', 'warnings']
可以用自訂模組 members 的 list_members() 顯示類別, 函式, 與子模組等成員, 參考 :
>>> from members import list_members
>>> list_members(importlib)
find_loader <class 'function'>
import_module <class 'function'>
invalidate_caches <class 'function'>
machinery <class 'module'>
reload <class 'function'>
sys <class 'module'>
util <class 'module'>
warnings <class 'module'>
可見 importlib 有四個函式與子模組 (find_loader 已被棄用), 常用函式如下表 :
| importlib 常用函式 | 功能說明 |
|---|---|
importlib.import_module(name) | 動態匯入模組, 效果與 import 相同, 可指定字串路徑. |
importlib.reload(module) | 重新載入已載入的模組, 常用於模組更新後的即時刷新. |
importlib.util.find_spec(name) | 傳回指定模組的 ModuleSpec, 可查詢路徑與 loader 等資訊. |
importlib.util.module_from_spec(spec) | 根據 spec 產生尚未初始化的模組物件. |
spec.loader.exec_module(module) | 將指定模組物件執行, 真正「啟動」模組內容. |
importlib.invalidate_caches() | 清除 importlib 的快取, 強迫重新尋找模組. |
1. 載入目前目錄下模組或系統模組 :
importlib.import_module() 函式用來依據傳入之模組名稱 (字串) 動態載入其他模組, 這些模組的位置必須在目前的工作目錄下或系統模組 (可以在 sys.path 找到的模組), 意即它們都在標準查找路徑內. 例如載入標準函式庫中的模組, 只要傳入內建函式名稱即可 :
>>> math_module=importlib.import_module('math')
>>> math_module.sqrt(2)
1.4142135623730951
此與直接匯入 math 做計算效果是一樣的 :
>>> import math
>>> math.sqrt(2)
1.4142135623730951
但差別在於傳入 importlib.import_module() 的是字串變數, 因此可以動態地載入任何模組, 而不須於程式開頭靜態載入 (事實上如果模組名稱未知, 而是在執行時才確定, 那也無法使用 import 靜態載入).
也可以呼叫內建函式 getattr() 從 importlib.import_module() 載入的模組中取出指定之物件或函式, 例如數學函式庫 math_module :
>>> sqrt_func=getattr(math_module, 'sqrt')
>>> sqrt_func(2)
1.4142135623730951
另外也可以載入目前工作目錄下的模組, 只要傳入模組名稱即可, 如果位於子目錄下則加上以點號隔開的路徑即可, 例如 '子目錄.孫目錄.模組名稱'.
例如在目前工作目錄下建立了一個子目錄 functions :
D:\python\test>mkdir functions
然後在底下分別編輯三個模組 add.py, sub.py, 與 mul.py :
# add.py
def calc(x, y):
return x + y
# sub.py
def calc(x, y):
return x - y
# mul.py
def calc(x, y):
return x * y
另外為了讓 functions 子目錄符合 Python 套件要求, 在 functions 子目錄下須新增一個空檔案 __init__.py, 檔案目錄結構如下圖 :
然後回上一層 (工作目錄), 新增一個 Python 檔案, 例如 dynamic_loading_1.py :
# dynamic_loading_1.py
import importlib
modules=['add', 'sub', 'mul'] # 模組清單
x, y=10, 3 # 傳入參數
# 執行每個模組的 calc() 函式
for module_name in modules:
module_path=f'functions.{module_name}' # 例如 'functions.add'
module=importlib.import_module(module_path)
result=module.calc(x, y)
print(f'{module_name}.calc({x}, {y})={result}')
此程式中的 modules 串列儲存了 functions 子目錄下的三個模組名稱, 然後用迴圈走訪此串列, 於迴圈中用 importlib.import_module() 依序載入此三模組實例, 呼叫其 calc() 函式進行運算後輸出, 結果如下 :
>>> %Run dynamic_loading_1.py
add.calc(10, 3)=13
sub.calc(10, 3)=7
mul.calc(10, 3)=30
此簡單範例說明了動態載入模組的原理, 在上面的應用程式 dynamic_loading_1.py 中的模組名稱為已知之串列內容, 但是在實際應用上 (例如 serverless 平台) functions 底下的模組名稱可能是動態變化的, 且真正要執行的模組是由客戶端傳來的變數, 無法用 import 靜態載入, 這時就必須用到此例所展示的動態載入技巧了.
2. 載入其他位置下的模組 :
如果要載入的模組既非系統模組 (即不在 sys.path 內) 也不在目前工作目錄或其子目錄下, 而是在其他任意位置的話 (僅知其檔案路徑), 就無法使用 importlib.import_module() 載入, 必須使用 importlib.util.spec_from_file_location() 搭配 importlib.util.module_from_spec() 才能載入.
在上面的範例中, 要載入的模組位於 D:\python\test\functions 資料夾下面 :
D:\python>tree test\functions /f
列出磁碟區 新增磁碟區 的資料夾 PATH
磁碟區序號為 1258-16B8
D:\PYTHON\TEST\FUNCTIONS
add.py
mul.py
sub.py
__init__.py
我在 D:\python\ 下建立一個與 test 平行的 test2 資料夾, 並且新建一個 dynamic_loading_2.py 應用程式來測試從任意位置載入模組的方法 :
D:\python>tree test2 /f
列出磁碟區 新增磁碟區 的資料夾 PATH
磁碟區序號為 1258-16B8
D:\PYTHON\TEST2
dynamic_loading_2.py
dynamic_loading_2.py 程式碼如下 :
# dynamic_loading_2.py
import importlib
import os
modules=['add', 'sub', 'mul'] # 模組清單
x, y=10, 3 # 傳入參數
# 定義 functions 資料夾的實際路徑(根據你的說明)
functions_dir=r'D:\python\test\functions'
# 動態載入並執行每個模組的 calc() 函式
for module_name in modules:
# 串接路徑與檔名
file_path=os.path.join(functions_dir, f'{module_name}.py')
# 從指定檔案路徑建立模組載入規格 (傳回 ModuleSpec 物件 )
spec=importlib.util.spec_from_file_location(module_name, file_path)
# 根據模組規格 spec 建立一個新的模組物件 (傳回 ModuleType 物件)
module=importlib.util.module_from_spec(spec)
# 執行模組程式碼 (將模組內容載入到記憶體中的 module 物件)
spec.loader.exec_module(module)
# 執行 module 物件中的 calc() 函式
result=module.calc(x, y)
print(f'{module_name}.calc({x}, {y})={result}')
此處先定義函式模組的資料夾變數 functions_dir, 然後在遍歷 modules 的迴圈中, 用 importlib.util.spec_from_file_location() 建立載入該模組的規格 (ModuleSpec) 物件, 然後呼叫 importlib.util.module_from_spec() 並傳入規格物件來建立一個模組物件, 最後呼叫 spec.loader.exec_module() 並傳入模組物件將模組載入記憶體, 這樣便能呼叫模組物件中的函式了, 結果如下 :
>>> %cd 'D:\python'
>>> %cd 'D:\python\test'
>>> %cd 'D:\python'
>>> %cd 'D:\python\test2'
>>> %Run dynamic_loading_2.py
add.calc(10, 3)=13
sub.calc(10, 3)=7
mul.calc(10, 3)=30
可見與上面範例一樣都能動態載入模組執行裡面的函式. 但此方法比上面的老, 因為可載入放在任何位置的 .py 模組檔案, 而且該模組不需要在 sys.path 中 (不用汙染 sys.path), 也不需位於合規套件內 (即含有 __init__.py).
PS :
加入 sys.path 的方法 :
import sys
sys.path.append('/home/你的使用者')

沒有留言 :
張貼留言