本文适合有 Python 基础、想了解架构设计、但不知道”微内核”在代码里长什么样的读者。
代码仓库:github.com/SumengQAQ/pythonic-design-patterns


一、微内核是什么?

先讲个故事。

你用 VS Code 写代码。VS Code 本身只提供编辑器窗口和几个菜单项。安装 Python 插件后,代码高亮了、能调试了。装 Git 插件后,侧边栏多了源代码管理。装 Live Share 插件后,你能跟朋友实时协作。

VS Code 本身很小,功能全是插件给的。

这就是微内核模式——一个极小的核心系统 + 一堆可插拔的插件。核心只做两件事:

  1. 管好插件(加载、注册、卸载)
  2. 提供插件之间通信的渠道

至于”格式化代码”、”语法高亮”、”调试”……这些都是插件的事,内核不管。


二、我们要做什么

作为一篇教程,总得有个看得见摸得着的目标。

我选了一个足够简单又足够说明问题的场景:模拟 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
# core/event_bus.py
from collections import defaultdict
from 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): # 打印:ChildA 继承了 Base!
pass

class ChildB(Base): # 打印: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()BaseMixin 的钩子会互相覆盖——只有继承顺序中最后一个父类的 __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):
# priority = 10,加载顺序靠前
...

class ReportPlugin(BasePlugin, priority=-5):
# 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
# core/registry.py
from __future__ import annotations
from typing import Type
from abc import ABC, abstractmethod


class 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 os
import importlib

def 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] # 去掉 .py
importlib.import_module(f"plugins.{module_name}")

5.1 importlib 做了什么

importlib.import_module("plugins.format") 等价于你手动写 import plugins.format

它做了三件事:

  1. 找到 plugins/format.py 文件
  2. 完整执行这个文件的全部代码
  3. 把文件里定义的名字挂在返回的 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
# plugins/format.py
from core.registry import BasePlugin
from core.event_bus import EventBus, RightClickEvent


class 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
# plugins/quickly_run.py
from core.registry import BasePlugin
from core.event_bus import EventBus, RightClickEvent


class 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
# main.py
from core.registry import PluginRegistry
from core.event_bus import EventBus, RightClickEvent
import importlib
import os


def 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" # 可改为 "per_player" 或 "per_level"
  • 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 可以在继承时传参数(priorityscope
super().__init_subclass__() 保持 MRO 链上的钩子传递
importlib 运行时动态加载 .py 文件
__file__ 用绝对路径避免工作目录问题
导入即执行 Python 没有”编译好等着”的阶段
生命周期 Singleton / Per-Player / Per-Level + OSGi 六状态

代码仓库:github.com/SumengQAQ/pythonic-design-patterns