Python - 利用 pkgutil & importlib 打造可擴充式模組
Posted on Apr 27, 2018 in Python 程式設計 - 高階 by Amo Chen ‐ 3 min read
想要建立好的 Python 專案結構,模組化是免不了的過程。此外,程式 / 系統如果還需要良好的擴充性,例如可以彈性增加 Plugin ,可以考慮利用 pkgutil 與 importlib 模組自動載入模組的做法。
Python 知名的 Open Source 專案 Cuckoo Sandbox 就是利用類似的做法達到可擴充性,讓人可以實作一個外掛模組,然後丟到特定資料夾後,它就能夠被執行。
本文將實作一個最小的可擴充式模組架構,所使用之 Python 版本為 3.5.2 。
首先,我們的 Python 專案資料夾可能會有類似以下的結構:
.
├── libs/
│ ├── base.py
│ └── __init__.py
├── modules/
│ ├── moduleB.py
│ ├── moduleA.py
│ └── __init__.py
└── main.py
上述檔案與資料夾的用途說明如下:
libs/
重要函式存放的資料夾libs/base.py
定義了所有modules/
資料夾下所有模組的父類別基本樣貌modules/
可彈性擴充模組的資料夾modules/modulesA.py
模組的範例main.py
Python 主程式
接下來,說明自動載入模組的主要關鍵做法,以下分點陳述:
- 在
base.py
中定義我們模組主要的方法與樣貌,例如以下 Python 程式碼是我們所定義的Base
類別,用來讓modules/
底下的模組繼承。
#! -*- coding: utf-8 -*-
class Base(object):
def __init__(self):
self.name = 'Base'
def run(self):
print(self.name, 'is running')
- 在
modules/
底下建立類別模組,並使之繼承我們第1步所建立的Base
類別,例如以下程式碼:
#! -*- coding: utf-8 -*-
from libs.base import Base
class ModuleA(Base):
def __init__(self):
self.name = 'ModuleA'
於
main.py
利用pkgutil.iter_modules
取得modules/
底下的所有模組名稱。於
main.py
使用importlib.import_module
方法將第 3 步所取得的模組名稱一個一個 import 進來。於
main.py
利用__subclass__
方法取得我們第 1 步所建立的Base
類別的所有子類別,即第 2 步所建立的子類別,也就是 Plugin 。於
main.py
一一執行這些類別共同定義的方法,例如範例中的run()
方法。
上述的 3-6 步驟 main.py 範例程式碼,如下所示:
#! -*- coding: utf-8 -*-
import pkgutil
import importlib
from libs.base import Base
def main():
for finder, name, _ in pkgutil.iter_modules(['modules']):
try:
importlib.import_module('{}.{}'.format(finder.path, name))
except Exception as e:
print('Can not import {}'.format(name))
for cls in Base.__subclasses__():
instance = cls()
instance.run()
if __name__ == '__main__':
main()
主要透過以上 6 個主要步驟,就能夠達成自動載入模組的目標。
其中 importlib.import_module
其實是包裝過的 importlib.__import__
,如果沒什麼特殊需求,用 importlib.import_module
來 import 模組即可。
p.s. importlib.import_module('{}.{}'.format(finder.path, name))
可以寫成 importlib.__import__(, globals(), locals(), ['dummy'], -1)
,而我們此處所使用的參數主要是將程式執行時的全域與區域變數一併傳入給 __import__
方法參考,並且為了確保 __import__
不會回傳最上層的 modules
模組(例如 modules.moduleA
,最上層的模組即為 modules
),因此我們傳入 ['dummy']
確保這個參數不為空值,以令其回傳 modules/
下的模組,引用官方說法如下:
When the name variable is of the form package.module, normally, the top-level package (the name up till the first dot) is returned, not the module named by name. However, when a non-empty fromlist argument is given, the module named by name is returned.
最後可以執行 main.py
,就可以看到 ModuleA 被自動載入並且執行 run 方法,執行結果:
$ python main.py
ModuleA is running
以上就是利用 pkgutil
& importlib
自動載入模組的方法。
後記
編者後來將動態載入的部分變成一個函式,整個流程就更加簡潔許多。同樣用以下的檔案資料夾結構為例。
.
├── libs/
│ ├── base.py
│ └── __init__.py
├── modules/
│ ├── moduleB.py
│ ├── moduleA.py
│ └── __init__.py
└── main.py
將 main.py 改為以下程式,也能夠達到同樣的功能:
#! -*- coding: utf-8 -*-
import pkgutil
import importlib
from libs.base import Base
def load_modules():
"""
Import all classes under the folder 'modules'.
>>> load_modules()
"""
for finder, name, _ in pkgutil.iter_modules(['modules']):
try:
importlib.import_module('{}.{}'.format(finder.path, name))
except ImportError as e:
logger.debug(e)
return Base.__subclasses__()
def main():
for cls in load_modules():
instance = cls()
instance.run()
if __name__ == '__main__':
main()
上述的 load_modules()
就是自動載入 modules
資料夾底下所有模組的函式。這個部分還可以寫得更靈活些,可以依照不同路徑自動載入等等。
只是這並非本文的目標,因此就不多花些篇幅解釋如何改進了。
References
https://docs.python.org/3/library/pkgutil.html
https://docs.python.org/3/library/importlib.html
https://docs.python.org/3/library/functions.html