关于用python实现可插拔架构

感觉插件这东西挺有意思的,找个项目学习一下。这篇是学习笔记,学点写点吧。

插件设计

在wukong-robot项目中,所谓插件也就是一个有着特殊格式的py文件。这个没什么硬性要求,都是自己规定的

插件的加载

普通模块or插件?

如何判断一个模块是插件还是普通模块?首先需要在项目中指定一个文件夹来存放插件,比如取名:Plugins ;然后所有的插件都要自定义一个特定的类, 比如定义为 Plugin.

通过下面的代码我可以遍历某个路径中所有的模块,并找出插件:

plugin_loader.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for finder, name, ispkg in pkgutil.walk_packages(locations):  #
try:
loader = finder.find_module(name)
mod = loader.load_module(name)
except Exception:
logger.warning("插件 {} 加载出错,跳过".format(name),
exc_info=True)
continue

if not hasattr(mod, 'Plugin'):
# 可以是 模块中有一个名叫Plugin的类。或者是 模块中写了 import Plugin 这句判断都会通过的
logger.info("模块 {} 非插件,跳过".format(name))
continue
# plugins run at query 在查询时运行
plugin = mod.Plugin(con) # 把这个插件实例化

if plugin.SLUG == 'AbstractPlugin': # 这行代码有什么用? 猜测是防止插件忘写SLUG? 感觉没什么必要
plugin.SLUG = name
if issubclass(mod.Plugin, AbstractPlugin): #加载插件 这个函数的作用是 判断当前插件是不是继承自Abstract
logger.info("插件 {} 加载成功 ".format(name))
_plugins_query.append(plugin)
else:
logger.info("插件 {} 父类不合格 ".format(name))

slug属性是在基类中定义好的,子类应该使用新的值覆盖它。这里是插件名,程序会根据slug在Plugin列表里查找的

walk_packages(): 它的能力是根据路径搜索路径包中所有的模块(我觉得应该是所有内容)并 返回module_finder, name和ispkg。

  1. module_finder: 它可以返回指定模块的loader对象,loader对象的load_module()方法又可以返回module对象。
  2. name:就很好理解了,它就是模块的名字,比如有模块test.py。name就是”test”
  3. ispkg: 也就是字面意思,反应这个东西是不是个包。个人猜测是因为包里也可能会套着包,所以需要分拣一下。

hasattr(): 它可以判断一个对象中是否有某个属性(Attribute)。

一个空模块的属性包括:

1
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']

接着我在模块中写了两个类 Test,Test1 ,还 import了个模块os,这时属性就变成

1
['Test', 'Test1', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'os']

由此我所得到的关键信息是:test模块中的类名和import的模块名是包含在属性列表里的。

具体条件

在上面的代码中,模块需要满足以下的条件才能被识别为插件:

  1. 模块包含属性”Plugin”, 这里是是指模块中有Plugin类
  2. 且Plugin类继承自AbstractPlugin类
  3. 类中有SLUG属性。 没有的话应该会报异常,这里没有try catch,全靠插件开发者遵守约定

插件的运行

在本项目中,插件的运行都靠brain.py. brain模块的运行是被动的,用户每说一次指令,brain.py的query方法就会运行一次。运行过程中它遍历插件list,尝试按照某种规则把命令与插件匹配。当用户发出的命令命中插件时,就调用该插件的handle()方法,插件的功能也就是在handle()中进行实现的。
关键代码如下:

brain.py
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
for plugin in self.plugins:  # 拿着 用户的话和NLU识别结果 试探所有插件
if not plugin.isValid(text, parsed):
continue
# 命中后 进入使用逻辑
logger.info("'{}' 命中技能 {}".format(text, plugin.SLUG))
if plugin.IS_IMMERSIVE: # 判断是不是沉浸模式
self.conversation.setImmersiveMode(plugin.SLUG)

continueHandle = False
try:
self.handling = True
continueHandle = plugin.handle(text, parsed) # 进入处理逻辑
self.handling = False
except Exception:
logger.critical('Failed to execute plugin',
exc_info=True)
reply = u"抱歉,插件{}出故障了,晚点再试试吧".format(plugin.SLUG)
self.conversation.speak(reply, plugin=plugin.SLUG)
else:
logger.debug("Handling of phrase '%s' by " +
"plugin '%s' completed", text,
plugin.SLUG)
finally:
if not continueHandle:
return True

logger.debug("No plugin was able to handle phrase {} ".format(text))
return False

注: handle方法返回 None

鼓励一下:D