本文适合有 Python 基础、想了解架构设计、但不知道”微内核”在代码里长什么样的读者。 代码仓库:github.com/SumengQAQ/pythonic-design-patterns
一、微内核是什么? 先讲个故事。
你用 VS Code 写代码。VS Code 本身只提供编辑器窗口和几个菜单项。安装 Python 插件后,代码高亮了、能调试了。装 Git 插件后,侧边栏多了源代码管理。装 Live Share 插件后,你能跟朋友实时协作。
VS Code 本身很小,功能全是插件给的。
这就是微内核模式——一个极小的核心系统 + 一堆可插拔的插件。核心只做两件事:
管好插件(加载、注册、卸载)
提供插件之间通信的渠道
至于”格式化代码”、”语法高亮”、”调试”……这些都是插件的事,内核不管。
二、我们要做什么 作为一篇教程,总得有个看得见摸得着的目标。
我选了一个足够简单又足够说明问题的场景:模拟 IDE 的右键菜单 。
当你右键点击一个文件时,不同插件会往菜单里添加不同选项:
1 2 3 右键点击文件 → 菜单弹出 ├── 格式化(Format 插件) └── 编译并运行(QuicklyRun 插件)
这个场景天然适合微内核:
内核负责”收到右键点击事件”→”通知插件”
插件负责”添加自己的菜单项”
互不干扰,各管各的
三、第一步:事件总线(插件之间的通信渠道) 插件之间不能直接调用——不然就耦合了。那一个右键点击事件来了,怎么让所有插件都知道?
需要一个事件总线(EventBus) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from collections import defaultdictfrom typing import Callable class EventBus : __events: dict [type , list [Callable ]] = defaultdict(list ) @classmethod def register (cls, event: type , callback: Callable ): """订阅事件""" cls.__events[event].append(callback) @classmethod def emit (cls, event: object ): """发布事件""" for callback in cls.__events[type (event)]: callback(event)class RightClickEvent : """右键点击事件——此处仅作演示,不携带属性""" ...
核心逻辑就两行:
register(event_type, callback) — 当 event_type 发生时,调 callback
emit(event) — 触发事件,调所有注册过的回调
所以当内核调用 EventBus.emit(RightClickEvent()) 时,所有订阅了右键事件的插件都会收到通知。
四、第二步:插件基类与自动注册 插件怎么注册到内核里?
最笨的办法:手动写 register("format_plugin", FormatPlugin)。
但这样每写一个新插件,你都要记得去某个地方”登记”一下。忘了怎么办?
Python 3.6 引入了一个机制——__init_subclass__ (PEP 487)。
4.1 __init_subclass__ 是什么? 简单说:当一个类继承了某个父类时,父类能立刻感知到 。
1 2 3 4 5 6 7 8 9 10 class Base : def __init_subclass__ (cls, **kwargs ): super ().__init_subclass__(**kwargs) print (f"{cls.__name__} 继承了 Base!" )class ChildA (Base ): pass class ChildB (Base ): pass
关键点:不是在”第一次调用时”触发,是在”类定义时”就触发。
写完 class ChildA(Base): 那一瞬间,__init_subclass__ 就已经执行完了。不需要实例化,不需要调任何方法。定义即注册。
这在微内核中非常有用——插件作者不需要学习”怎么注册插件”,只要继承 BasePlugin,系统自动就把它登记上了。
4.2 super().__init_subclass__() 为什么不能省 1 2 3 4 5 6 7 8 9 10 class Base : def __init_subclass__ (cls, **kwargs ): print (f"Base 感知到 {cls.__name__} " )class Mixin : def __init_subclass__ (cls, **kwargs ): print (f"Mixin 感知到 {cls.__name__} " )class MyPlugin (Base, Mixin): pass
如果没有 super(),Base 和 Mixin 的钩子会互相覆盖——只有继承顺序中最后一个父类的 __init_subclass__ 会生效。
有了 super().__init_subclass__(**kwargs),它会沿着 MRO(方法解析顺序)链传递,两个父类都能感知到。
4.3 PEP 487 还带来了什么? PEP 487 不只引入了 __init_subclass__,还引入了自定义类创建时的关键字参数 :
1 2 3 4 5 6 7 8 9 10 11 12 13 class BasePlugin : def __init_subclass__ (cls, priority=0 , **kwargs ): super ().__init_subclass__(**kwargs) cls.priority = priority PluginRegistry.register(cls)class FormatPlugin (BasePlugin, priority=10 ): ...class ReportPlugin (BasePlugin, priority=-5 ): ...
这样你可以在定义插件时直接指定优先级和生命周期策略,不需要额外配置。
1 2 3 4 5 6 7 8 class BasePlugin : scope = "singleton" def __init_subclass__ (cls, scope="singleton" , priority=0 , **kwargs ): super ().__init_subclass__(**kwargs) cls.scope = scope cls.priority = priority PluginRegistry.register(cls)
PEP 487 让你的插件声明变得更像”声明式 API”,而不是”过程式注册”。
4.4 完整实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 from __future__ import annotationsfrom typing import Type from abc import ABC, abstractmethodclass PluginRegistry : _plugins: dict [str , Type [BasePlugin]] = {} _order: list [str ] = [] @classmethod def register (cls, plugin ) -> None : if not plugin.name: raise ValueError(f"插件 {plugin.__name__} 需要 name 属性" ) cls._plugins[plugin.name] = plugin @classmethod def get (cls, plugin_name: str ) -> Type [BasePlugin]: if plugin_name not in cls._plugins: raise ValueError(f"未找到 {plugin_name} 插件" ) return cls._plugins[plugin_name] @classmethod def get_all (cls ) -> list [str ]: """获取所有插件名称,按优先级排序""" return sorted ( cls._plugins.keys(), key=lambda name: cls._plugins[name].priority, reverse=True )class BasePlugin (ABC ): name: str priority: int = 0 scope: str = "singleton" def __init_subclass__ (cls, priority=0 , scope="singleton" , **kwargs ): super ().__init_subclass__(**kwargs) cls.priority = priority cls.scope = scope PluginRegistry.register(cls) @classmethod @abstractmethod def initialize (cls ): """插件初始化入口——子类必须实现""" ...
五、第三步:插件自动发现(importlib) 插件注册好了,但 PluginRegistry 里是空的。怎么让 Python 真正把插件文件加载进来?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import osimport importlibdef load_all_plugins (): """自动扫描并加载 plugins/ 目录下的所有插件""" base_dir = os.path.dirname(os.path.abspath(__file__)) plugin_dir = os.path.join(base_dir, "plugins" ) for file_name in os.listdir(plugin_dir): if not file_name.endswith(".py" ) or file_name.startswith("_" ): continue module_name = file_name[:-3 ] importlib.import_module(f"plugins.{module_name} " )
5.1 importlib 做了什么 importlib.import_module("plugins.format") 等价于你手动写 import plugins.format。
它做了三件事:
找到 plugins/format.py 文件
完整执行 这个文件的全部代码
把文件里定义的名字挂在返回的 module 对象上
注意第 2 点——是完整执行。 当你 from plugins.format import FormatPlugin 时,Python 还是会从头到尾跑一遍 format.py,只是最后只给你 FormatPlugin 这个名字。不能只加载一个类而不执行文件中的其他代码。这是 Python 解释器的工作方式决定的。
5.2 一个常见的坑 1 2 3 4 5 6 7 8 9 for file_name in os.listdir("plugins" ): ... base_dir = os.path.dirname(os.path.abspath(__file__)) plugin_dir = os.path.join(base_dir, "plugins" )for file_name in os.listdir(plugin_dir): ...
os.listdir("plugins") 找的是当前工作目录 ——也就是你运行 python main.py 时的目录。
如果从项目根目录运行没问题,但如果换了一个目录启动程序,就找不到 plugins 文件夹了。
用 os.path.dirname(__file__) 获取 main.py 所在目录,然后构建绝对路径,不管在哪运行都不会出错。
六、第四步:写两个插件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from core.registry import BasePluginfrom core.event_bus import EventBus, RightClickEventclass FormatPlugin (BasePlugin ): """在点击右键时,添加"格式化"菜单项""" name = "format" @classmethod def initialize (cls ): EventBus.register(RightClickEvent, cls.show_option) @staticmethod def show_option (event: RightClickEvent ): print ("> 格式化" )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from core.registry import BasePluginfrom core.event_bus import EventBus, RightClickEventclass QuicklyRunPlugin (BasePlugin ): """在点击右键时,添加"编译并运行"菜单项""" name = "quickly_run" @classmethod def initialize (cls ): EventBus.register(RightClickEvent, cls.show_option) @staticmethod def show_option (event: RightClickEvent ): print ("> 编译并运行" )
每个插件约 15 行,不需要改任何内核代码。加一个新插件只需要在 plugins/ 下新建一个文件,继承 BasePlugin,实现 initialize。
七、组装起来 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from core.registry import PluginRegistryfrom core.event_bus import EventBus, RightClickEventimport importlibimport osdef load_all_plugins (): base_dir = os.path.dirname(os.path.abspath(__file__)) plugin_dir = os.path.join(base_dir, "plugins" ) for file_name in os.listdir(plugin_dir): if not file_name.endswith(".py" ) or file_name.startswith("_" ): continue module_name = file_name[:-3 ] importlib.import_module(f"plugins.{module_name} " ) for name in PluginRegistry.get_all(): PluginRegistry.get(name).initialize()def main (): load_all_plugins() print ("模拟点击右键" ) EventBus.emit(RightClickEvent())if __name__ == "__main__" : main()
运行结果:
1 2 3 4 5 格式化插件初始化 快速运行插件初始化 模拟点击右键 > 格式化 > 编译并运行
八、项目结构 1 2 3 4 5 6 7 8 微内核/ ├── main.py # 入口 ├── core/ # 微内核(约 50 行) │ ├── event_bus.py # 事件总线 │ └── registry.py # 插件注册表 + BasePlugin └── plugins/ # 插件目录(每个约 15 行) ├── format.py └── quickly_run.py
九、还没解决的:插件生命周期管理 这是目前微内核最复杂的部分,我还没有完全实现。
存类还是存实例?
无状态插件(如 FormatPlugin)→ 存类就行,每次调用用类方法
有状态插件(如 TimerPlugin,每个玩家有自己的计时器)→ 需要存实例
实例的作用域? 1 2 class BasePlugin : scope: str = "singleton"
singleton — 全局唯一,所有场景共享
per_player — 每个玩家独立一份(比如每个玩家有自己的背包)
per_level — 每个关卡重新创建(比如每个关卡有不同的掉落物)
卸载与清理 当插件被禁用时,需要取消注册事件回调、释放资源。需要一个 shutdown() 方法:
1 2 3 4 5 6 7 8 9 class BasePlugin (ABC ): @classmethod @abstractmethod def initialize (cls ): ... @classmethod def shutdown (cls ): """清理资源,默认空实现""" pass
OSGi 的六个状态 Java 的 OSGi 规范定义了插件的六个生命周期状态:
1 INSTALLED → RESOLVED → STARTING → ACTIVE → STOPPING → UNINSTALLED
每个状态能做什么、不能做什么,全写死了。不允许跳过、不允许逆向。
我花了一晚上重新推演了 OSGi 那群架构师二十年前走过的路,然后得出结论:先把这篇教程发出去,这个坑以后再填。 🫠
十、总结
知识点
一句话
微内核
一个极小的核心 + 一堆可插拔的插件
EventBus
插件之间不直接调用,通过事件通信
__init_subclass__
定义即注册,插件不需要手动登记
PEP 487
可以在继承时传参数(priority、scope)
super().__init_subclass__()
保持 MRO 链上的钩子传递
importlib
运行时动态加载 .py 文件
__file__
用绝对路径避免工作目录问题
导入即执行
Python 没有”编译好等着”的阶段
生命周期
Singleton / Per-Player / Per-Level + OSGi 六状态
代码仓库:github.com/SumengQAQ/pythonic-design-patterns