瞭解過編程的人應該對函數重寫 ( override ) 不陌生,但其實,這個普適的方法並不適用於全部的應用場景。舉個簡單的例子,當多個項目代碼貢獻方都想參與同一程序的修改時,頻繁的函數重寫會使代碼變得異常混亂,讓整個項目變得難以維護。python
那麼,有沒有更優雅的方法可以兼顧代碼的擴展性與穩定性呢?編程
有的,pytest ( python 單元測試框架 ) 的做者就意識到了這個問題。在其源碼中,能夠發現許多通過 @pytest.hookimpl 關鍵字裝飾的函數,這表明這個函數是一個插件的實現,其做用是經過用插件調用的形式來替代函數重寫。。windows
pytest 部分源碼:設計模式
@pytest.hookimpl(hookwrapper=True)
def pytest_load_initial_conftests(early_config: Config):
ns = early_config.known_args_namespace
if ns.capture == "fd":
_py36_windowsconsoleio_workaround(sys.stdout)
_colorama_workaround()
_readline_workaround()
pluginmanager = early_config.pluginmanager
capman = CaptureManager(ns.capture)
pluginmanager.register(capman, "capturemanager")
# make sure that capturemanager is properly reset at final shutdown
early_config.add_cleanup(capman.stop_global_capturing)
# finally trigger conftest loading but while capturing (issue93)
capman.start_global_capturing()
outcome = yield
capman.suspend_global_capture()
if outcome.excinfo is not None:
out, err = capman.read_global_capture()
sys.stdout.write(out)
sys.stderr.write(err)
複製代碼
早前在 pytest 中這只是一個插件工具庫,而隨着這個庫的日益發展, 做者把它從 pytest 中分離了出來,並將其命名爲了 pluggy。app
pluggy 是 pytest 中插件管理和鉤子函數調用的核心組件。它已經讓 500 多個插件自由擴展和自定義了 pytest 的默認行爲。甚至能夠說 pytest 自己就是根據良好的規範協議依次調用的插件的集合。框架
經過爲主程序安裝插件,它使用戶可以擴展或修改主程序的行爲。插件代碼將做爲正常程序執行的一部分運行,從而更改或加強程序的某些功能。ide
本質上,pluggy 讓全部函數 "鉤子化",所以使用者能夠用其輕鬆構建 "可插拔" 系統。函數
當有多方想要定製化擴展程序時,使用函數重寫的方式將會致使程序代碼異常混亂。而經過 pluggy 可使獲得主程序與插件之間鬆耦合。工具
pluggy 也讓主程序的設計者去仔細考慮鉤子函數實現中須要用到哪些對象。而這爲插件建立者提供了一個清晰的框架,由於它說明了如何經過一組定義良好的函數和對象來擴展主程序,最終讓整個項目代碼框架結構愈來愈清晰。單元測試
首先咱們須要瞭解兩個概念:
主程序:經過指定鉤子函數並在程序執行過程當中調用其實現來提供可擴展性的程序;
插件:程序實現指定的鉤子(的一部分)並在主程序調用實現時參與程序執行。
而 pluggy 的做用則是經過使用如下幾個方面 聯通主程序與插件:
來演示一下 pluggy 的核心功能 - 即插即用。
import pluggy
hookspec = pluggy.HookspecMarker("myproject")
hookimpl = pluggy.HookimplMarker("myproject")
class MySpec(object):
"""A hook specification namespace."""
@hookspec
def myhook(self, arg1, arg2):
"""My special little hook that you can customize."""
class Plugin_1(object):
"""A hook implementation namespace."""
@hookimpl
def myhook(self, arg1, arg2):
print("inside Plugin_1.myhook()")
return arg1 + arg2
class Plugin_2(object):
"""A 2nd hook implementation namespace."""
@hookimpl
def myhook(self, arg1, arg2):
print("inside Plugin_2.myhook()")
return arg1 - arg2
# create a manager and add the spec
pm = pluggy.PluginManager("myproject")
pm.add_hookspecs(MySpec)
# register plugins
pm.register(Plugin_1())
pm.register(Plugin_2())
# call our `myhook` hook
results = pm.hook.myhook(arg1=1, arg2=2)
print(results)
複製代碼
在上述代碼中,開發者
定義了主程序提供的調用簽名 (myproject)對應的鉤子規範 - hookspec;
定義了鉤子實現 - hookimpl;
在 MySpec 中定義了 myhook 的入參;
在 Plugin_1 和 Plugin_2 中定義了 myhook 具體實現;
新建了插件管理對象 pm;
在 pm 中添加設置好的鉤子規格 MySpec;
在 pm 中註冊了定義好的 Plugin_1 和 Plugin_2;
經過 pm 進行了 myhook 的調用。
運行代碼後控制檯輸出以下:
inside Plugin_2.myhook()
inside Plugin_1.myhook()
[-1, 3]
複製代碼
爲何輸出是 [-1,3] 呢? 其實咱們能夠看出兩個插件中實現了相同的函數名,根據插件調用的先進後出原則,在調用 myhook 後程序會先執行後註冊的 Plugin_2.myhook(),後執行先註冊的 Plugin_1.myhook()。因此在最終的返回結果中,會得到按執行順序排列的返回值列表,其值分別是 -1 和 3。
通過了一段時間的學習,在親身實踐中確實領會到了插件化給系統帶來的益處。它不只給予了程序無限擴展的可能,也爲代碼賦予了穩定的基礎結構。這很是重要,由於若是地基不夠穩固,程序大廈將會在某個時刻瞬間崩塌。
現實中,其實全部的程序都在跟隨着實際業務不停地發展。因此歷來都沒有一勞永逸的設計模式,只有不停地在優化着的代碼結構。當一種編碼方式沒法知足現狀、知足需求時。總會有人去推翻它,去構建屬於這個時代的新模式。而這,正是編程的魅力所在。
以爲本文有幫助能夠點個贊哦,也歡迎關注個人公衆號。