3.5.4 插件系統設計
3.5.4 插件系統設計模式三:基於發現(使用
插件系統讓應用程式可以在不修改核心程式碼的情況下擴展功能。本章介紹三種常見的插件架構模式,以及如何使用 Python 的動態載入機制實現它們。
先備知識
- 本進階系列 模組二:元編程
- 特別是 Metaclass 與
__init_subclass__
插件架構模式
模式一:基於繼承
最簡單的插件模式,插件必須繼承基類:
1from abc import ABC, abstractmethod
2from typing import ClassVar
3
4class Plugin(ABC):
5 """插件基類"""
6 name: ClassVar[str] # 每個插件必須定義名稱
7
8 @abstractmethod
9 def execute(self, context: dict) -> dict:
10 """執行插件邏輯"""
11 ...
12
13class LoggingPlugin(Plugin):
14 name = "logging"
15
16 def execute(self, context: dict) -> dict:
17 print(f"Processing: {context}")
18 return context
19
20class ValidationPlugin(Plugin):
21 name = "validation"
22
23 def execute(self, context: dict) -> dict:
24 if not context.get("valid"):
25 raise ValueError("Invalid context")
26 return context優點:簡單明確,IDE 支援好 缺點:強制耦合,無法使用第三方類別
模式二:基於註冊
使用裝飾器或顯式註冊,更靈活:
1from typing import Callable, TypeAlias
2
3PluginFunc: TypeAlias = Callable[[dict], dict]
4
5class PluginRegistry:
6 """插件註冊表"""
7
8 def __init__(self) -> None:
9 self._plugins: dict[str, PluginFunc] = {}
10
11 def register(self, name: str) -> Callable[[PluginFunc], PluginFunc]:
12 """裝飾器:註冊插件"""
13 def decorator(func: PluginFunc) -> PluginFunc:
14 if name in self._plugins:
15 raise ValueError(f"Plugin '{name}' already registered")
16 self._plugins[name] = func
17 return func
18 return decorator
19
20 def get(self, name: str) -> PluginFunc | None:
21 return self._plugins.get(name)
22
23 def list_plugins(self) -> list[str]:
24 return list(self._plugins.keys())
25
26# 全域註冊表
27registry = PluginRegistry()
28
29@registry.register("uppercase")
30def uppercase_plugin(context: dict) -> dict:
31 context["text"] = context.get("text", "").upper()
32 return context
33
34@registry.register("lowercase")
35def lowercase_plugin(context: dict) -> dict:
36 context["text"] = context.get("text", "").lower()
37 return context
38
39# 使用
40plugin = registry.get("uppercase")
41if plugin:
42 result = plugin({"text": "Hello"})優點:靈活,支援函式和類別 缺點:需要顯式註冊
模式三:基於發現(使用 __init_subclass__)
利用 Python 的元編程機制自動發現插件:
1from typing import ClassVar
2
3class AutoRegisterPlugin:
4 """自動註冊的插件基類"""
5 _registry: ClassVar[dict[str, type["AutoRegisterPlugin"]]] = {}
6 name: ClassVar[str]
7
8 def __init_subclass__(cls, **kwargs) -> None:
9 super().__init_subclass__(**kwargs)
10 # 跳過沒有定義 name 的中間類別
11 if hasattr(cls, "name") and cls.name:
12 if cls.name in AutoRegisterPlugin._registry:
13 raise ValueError(f"Plugin '{cls.name}' already registered")
14 AutoRegisterPlugin._registry[cls.name] = cls
15
16 @classmethod
17 def get_plugin(cls, name: str) -> type["AutoRegisterPlugin"] | None:
18 return cls._registry.get(name)
19
20 @classmethod
21 def list_plugins(cls) -> list[str]:
22 return list(cls._registry.keys())
23
24 def execute(self, context: dict) -> dict:
25 raise NotImplementedError
26
27# 定義插件只需要繼承,自動註冊
28class FormatPlugin(AutoRegisterPlugin):
29 name = "format"
30
31 def execute(self, context: dict) -> dict:
32 context["formatted"] = True
33 return context
34
35class CompressPlugin(AutoRegisterPlugin):
36 name = "compress"
37
38 def execute(self, context: dict) -> dict:
39 context["compressed"] = True
40 return context
41
42# 自動發現
43print(AutoRegisterPlugin.list_plugins()) # ['format', 'compress']優點:無需顯式註冊,只要繼承就會被發現 缺點:必須繼承基類,Python 專屬
使用 Metaclass 的進階版本
1from typing import ClassVar
2
3class PluginMeta(type):
4 """插件元類別"""
5 _registry: dict[str, type] = {}
6
7 def __new__(mcs, name: str, bases: tuple, namespace: dict) -> type:
8 cls = super().__new__(mcs, name, bases, namespace)
9
10 # 跳過基類
11 if bases and hasattr(cls, "plugin_name"):
12 plugin_name = cls.plugin_name
13 if plugin_name in mcs._registry:
14 raise ValueError(f"Plugin '{plugin_name}' already registered")
15 mcs._registry[plugin_name] = cls
16
17 return cls
18
19 @classmethod
20 def get_plugin(mcs, name: str) -> type | None:
21 return mcs._registry.get(name)
22
23class PluginBase(metaclass=PluginMeta):
24 """插件基類"""
25 plugin_name: ClassVar[str]
26
27 def run(self) -> None:
28 raise NotImplementedError
29
30class MyPlugin(PluginBase):
31 plugin_name = "my_plugin"
32
33 def run(self) -> None:
34 print("MyPlugin running")
35
36# 使用
37plugin_cls = PluginMeta.get_plugin("my_plugin")
38if plugin_cls:
39 plugin_cls().run()動態載入模組
importlib 基礎
1import importlib
2from types import ModuleType
3
4def load_module(module_path: str) -> ModuleType:
5 """動態載入模組"""
6 return importlib.import_module(module_path)
7
8def load_class(module_path: str, class_name: str) -> type:
9 """從模組載入類別"""
10 module = importlib.import_module(module_path)
11 return getattr(module, class_name)
12
13# 使用
14# 假設有 myapp/plugins/custom.py 定義了 CustomPlugin
15plugin_cls = load_class("myapp.plugins.custom", "CustomPlugin")
16plugin = plugin_cls()從檔案路徑載入
1import importlib.util
2from pathlib import Path
3from types import ModuleType
4
5def load_module_from_path(path: Path) -> ModuleType:
6 """從檔案路徑載入模組"""
7 spec = importlib.util.spec_from_file_location(
8 path.stem, # 模組名稱
9 path # 檔案路徑
10 )
11 if spec is None or spec.loader is None:
12 raise ImportError(f"Cannot load module from {path}")
13
14 module = importlib.util.module_from_spec(spec)
15 spec.loader.exec_module(module)
16 return module
17
18# 使用
19plugin_module = load_module_from_path(Path("./plugins/custom_plugin.py"))importlib.metadata 的 entry_points(現代做法)
這是 Python 套件生態系統推薦的插件發現機制:
1# 在 pyproject.toml 中定義 entry points
2"""
3[project.entry-points."myapp.plugins"]
4format = "myapp_format_plugin:FormatPlugin"
5compress = "myapp_compress_plugin:CompressPlugin"
6"""
7
8from importlib.metadata import entry_points
9
10def discover_plugins(group: str) -> dict[str, type]:
11 """發現已安裝套件中的插件"""
12 plugins = {}
13
14 # Python 3.10+
15 eps = entry_points(group=group)
16
17 for ep in eps:
18 try:
19 plugin_cls = ep.load()
20 plugins[ep.name] = plugin_cls
21 except Exception as e:
22 print(f"Failed to load plugin {ep.name}: {e}")
23
24 return plugins
25
26# 發現所有 myapp.plugins 群組的插件
27plugins = discover_plugins("myapp.plugins")
28for name, cls in plugins.items():
29 print(f"Found plugin: {name} -> {cls}")優點:
- 標準化的發現機制
- 支援第三方套件提供插件
- pip 安裝即可使用
實際範例一:Hook 系統的插件化改造
基於入門系列的 Hook 系統概念,設計一個插件化的 Hook 框架:
1from abc import ABC, abstractmethod
2from dataclasses import dataclass
3from enum import Enum
4from pathlib import Path
5from typing import ClassVar
6import importlib.util
7
8class HookEvent(Enum):
9 PRE_TOOL_USE = "PreToolUse"
10 POST_TOOL_USE = "PostToolUse"
11 SESSION_START = "SessionStart"
12 SESSION_END = "SessionEnd"
13
14@dataclass
15class HookContext:
16 """Hook 執行的上下文"""
17 event: HookEvent
18 tool_name: str | None = None
19 input_data: dict | None = None
20
21@dataclass
22class HookResult:
23 """Hook 執行結果"""
24 success: bool
25 message: str
26 modified_context: HookContext | None = None
27
28class HookPlugin(ABC):
29 """Hook 插件基類"""
30 _registry: ClassVar[dict[str, list[type["HookPlugin"]]]] = {}
31
32 # 子類別必須定義的屬性
33 name: ClassVar[str]
34 events: ClassVar[list[HookEvent]]
35 priority: ClassVar[int] = 100 # 預設優先級
36
37 def __init_subclass__(cls, **kwargs) -> None:
38 super().__init_subclass__(**kwargs)
39 if hasattr(cls, "name") and hasattr(cls, "events"):
40 for event in cls.events:
41 event_name = event.value
42 if event_name not in HookPlugin._registry:
43 HookPlugin._registry[event_name] = []
44 HookPlugin._registry[event_name].append(cls)
45 # 按優先級排序
46 HookPlugin._registry[event_name].sort(
47 key=lambda c: c.priority
48 )
49
50 @abstractmethod
51 def execute(self, context: HookContext) -> HookResult:
52 """執行 Hook 邏輯"""
53 ...
54
55 @classmethod
56 def get_hooks_for_event(cls, event: HookEvent) -> list[type["HookPlugin"]]:
57 """取得特定事件的所有 Hook"""
58 return cls._registry.get(event.value, [])
59
60# 定義具體的 Hook 插件
61class SecurityCheckHook(HookPlugin):
62 """安全檢查 Hook"""
63 name = "security_check"
64 events = [HookEvent.PRE_TOOL_USE]
65 priority = 10 # 高優先級,先執行
66
67 DANGEROUS_PATTERNS = ["rm -rf", "DROP TABLE", "sudo"]
68
69 def execute(self, context: HookContext) -> HookResult:
70 if context.input_data:
71 command = context.input_data.get("command", "")
72 for pattern in self.DANGEROUS_PATTERNS:
73 if pattern in command:
74 return HookResult(
75 success=False,
76 message=f"Blocked dangerous pattern: {pattern}"
77 )
78 return HookResult(success=True, message="Security check passed")
79
80class LoggingHook(HookPlugin):
81 """日誌記錄 Hook"""
82 name = "logging"
83 events = [HookEvent.PRE_TOOL_USE, HookEvent.POST_TOOL_USE]
84 priority = 1000 # 低優先級,最後執行
85
86 def execute(self, context: HookContext) -> HookResult:
87 print(f"[{context.event.value}] Tool: {context.tool_name}")
88 return HookResult(success=True, message="Logged")
89
90# Hook 執行器
91class HookRunner:
92 """執行 Hook 的管理器"""
93
94 def run_hooks(self, event: HookEvent, context: HookContext) -> list[HookResult]:
95 """執行特定事件的所有 Hook"""
96 results = []
97 hook_classes = HookPlugin.get_hooks_for_event(event)
98
99 for hook_cls in hook_classes:
100 hook = hook_cls()
101 try:
102 result = hook.execute(context)
103 results.append(result)
104 # 如果 Hook 失敗且事件是 PRE,中斷執行
105 if not result.success and event == HookEvent.PRE_TOOL_USE:
106 break
107 except Exception as e:
108 results.append(HookResult(
109 success=False,
110 message=f"Hook {hook_cls.name} failed: {e}"
111 ))
112
113 return results
114
115# 使用
116runner = HookRunner()
117context = HookContext(
118 event=HookEvent.PRE_TOOL_USE,
119 tool_name="Bash",
120 input_data={"command": "ls -la"}
121)
122results = runner.run_hooks(HookEvent.PRE_TOOL_USE, context)實際範例二:通用插件框架設計
一個更通用的插件框架,支援動態載入和生命週期管理:
1from abc import ABC, abstractmethod
2from dataclasses import dataclass, field
3from enum import Enum, auto
4from pathlib import Path
5from typing import Any
6import importlib.util
7
8class PluginState(Enum):
9 UNLOADED = auto()
10 LOADED = auto()
11 ACTIVE = auto()
12 ERROR = auto()
13
14@dataclass
15class PluginInfo:
16 """插件元資訊"""
17 name: str
18 version: str
19 description: str
20 author: str = ""
21 dependencies: list[str] = field(default_factory=list)
22
23class BasePlugin(ABC):
24 """插件基類"""
25
26 @property
27 @abstractmethod
28 def info(self) -> PluginInfo:
29 """返回插件資訊"""
30 ...
31
32 def on_load(self) -> None:
33 """插件載入時呼叫"""
34 pass
35
36 def on_unload(self) -> None:
37 """插件卸載時呼叫"""
38 pass
39
40 def on_activate(self) -> None:
41 """插件啟用時呼叫"""
42 pass
43
44 def on_deactivate(self) -> None:
45 """插件停用時呼叫"""
46 pass
47
48@dataclass
49class LoadedPlugin:
50 """已載入的插件包裝"""
51 plugin: BasePlugin
52 state: PluginState = PluginState.UNLOADED
53 error: str | None = None
54
55class PluginManager:
56 """插件管理器"""
57
58 def __init__(self, plugin_dir: Path | None = None) -> None:
59 self._plugins: dict[str, LoadedPlugin] = {}
60 self._plugin_dir = plugin_dir
61
62 def load_plugin(self, path: Path) -> str:
63 """從檔案載入插件"""
64 try:
65 # 動態載入模組
66 spec = importlib.util.spec_from_file_location(
67 path.stem, path
68 )
69 if spec is None or spec.loader is None:
70 raise ImportError(f"Cannot load {path}")
71
72 module = importlib.util.module_from_spec(spec)
73 spec.loader.exec_module(module)
74
75 # 尋找 BasePlugin 的子類別
76 plugin_cls = None
77 for attr_name in dir(module):
78 attr = getattr(module, attr_name)
79 if (isinstance(attr, type) and
80 issubclass(attr, BasePlugin) and
81 attr is not BasePlugin):
82 plugin_cls = attr
83 break
84
85 if plugin_cls is None:
86 raise ImportError(f"No plugin class found in {path}")
87
88 # 實例化插件
89 plugin = plugin_cls()
90 plugin_name = plugin.info.name
91
92 # 檢查依賴
93 for dep in plugin.info.dependencies:
94 if dep not in self._plugins:
95 raise ImportError(f"Missing dependency: {dep}")
96
97 # 呼叫生命週期方法
98 plugin.on_load()
99
100 self._plugins[plugin_name] = LoadedPlugin(
101 plugin=plugin,
102 state=PluginState.LOADED
103 )
104
105 return plugin_name
106
107 except Exception as e:
108 raise ImportError(f"Failed to load plugin from {path}: {e}")
109
110 def unload_plugin(self, name: str) -> None:
111 """卸載插件"""
112 if name not in self._plugins:
113 raise KeyError(f"Plugin '{name}' not found")
114
115 loaded = self._plugins[name]
116
117 # 先停用
118 if loaded.state == PluginState.ACTIVE:
119 self.deactivate_plugin(name)
120
121 # 呼叫生命週期方法
122 loaded.plugin.on_unload()
123
124 del self._plugins[name]
125
126 def activate_plugin(self, name: str) -> None:
127 """啟用插件"""
128 if name not in self._plugins:
129 raise KeyError(f"Plugin '{name}' not found")
130
131 loaded = self._plugins[name]
132 if loaded.state != PluginState.LOADED:
133 raise RuntimeError(f"Plugin '{name}' is not in LOADED state")
134
135 try:
136 loaded.plugin.on_activate()
137 loaded.state = PluginState.ACTIVE
138 except Exception as e:
139 loaded.state = PluginState.ERROR
140 loaded.error = str(e)
141 raise
142
143 def deactivate_plugin(self, name: str) -> None:
144 """停用插件"""
145 if name not in self._plugins:
146 raise KeyError(f"Plugin '{name}' not found")
147
148 loaded = self._plugins[name]
149 if loaded.state != PluginState.ACTIVE:
150 return
151
152 loaded.plugin.on_deactivate()
153 loaded.state = PluginState.LOADED
154
155 def discover_plugins(self) -> list[Path]:
156 """發現插件目錄中的所有插件"""
157 if self._plugin_dir is None:
158 return []
159
160 return list(self._plugin_dir.glob("*.py"))
161
162 def load_all_plugins(self) -> dict[str, str | None]:
163 """載入所有發現的插件"""
164 results: dict[str, str | None] = {}
165 for path in self.discover_plugins():
166 try:
167 name = self.load_plugin(path)
168 results[name] = None
169 except Exception as e:
170 results[path.stem] = str(e)
171 return results
172
173 def get_plugin(self, name: str) -> BasePlugin | None:
174 """取得插件實例"""
175 loaded = self._plugins.get(name)
176 return loaded.plugin if loaded else None
177
178 def list_plugins(self) -> list[PluginInfo]:
179 """列出所有已載入的插件"""
180 return [lp.plugin.info for lp in self._plugins.values()]
181
182# 範例插件(放在獨立檔案中)
183class GreeterPlugin(BasePlugin):
184 """簡單的問候插件"""
185
186 @property
187 def info(self) -> PluginInfo:
188 return PluginInfo(
189 name="greeter",
190 version="1.0.0",
191 description="A simple greeting plugin"
192 )
193
194 def on_activate(self) -> None:
195 print("Greeter plugin activated!")
196
197 def greet(self, name: str) -> str:
198 return f"Hello, {name}!"
199
200# 使用
201manager = PluginManager(Path("./plugins"))
202manager.load_all_plugins()
203manager.activate_plugin("greeter")
204
205greeter = manager.get_plugin("greeter")
206if isinstance(greeter, GreeterPlugin):
207 print(greeter.greet("World"))設計考量
安全性
1# 限制插件可以存取的模組
2import sys
3from types import ModuleType
4
5class SandboxedImporter:
6 """限制插件可以 import 的模組"""
7 ALLOWED_MODULES = {"json", "re", "datetime", "typing"}
8
9 def find_module(self, name: str, path=None):
10 if name.split(".")[0] not in self.ALLOWED_MODULES:
11 return self # 攔截
12 return None
13
14 def load_module(self, name: str) -> ModuleType:
15 raise ImportError(f"Module '{name}' is not allowed in plugins")
16
17# 使用時插入到 sys.meta_path
18# sys.meta_path.insert(0, SandboxedImporter())版本相容性
1from packaging import version
2
3def check_compatibility(
4 plugin_info: PluginInfo,
5 app_version: str
6) -> bool:
7 """檢查插件與應用程式版本相容性"""
8 # 插件可以定義 min_app_version 屬性
9 min_version = getattr(plugin_info, "min_app_version", "0.0.0")
10 return version.parse(app_version) >= version.parse(min_version)小結
| 模式 | 適用場景 |
|---|---|
| 基於繼承 | 簡單場景,需要型別安全 |
| 基於註冊 | 需要靈活性,支援函式插件 |
| 基於發現 | 自動發現,減少樣板程式碼 |
| entry_points | 套件生態系統,第三方插件 |
思考題
- 三種插件模式各有什麼優缺點?
- 如何實現插件之間的依賴管理?
- 如何確保插件的安全性?
上一章:3.5.3 進階上下文管理 下一章:3.5.5 設計模式整合案例