我最近一直在写大量的 Python 代码,其中我非常欣赏的一点是,实现插件系统是多么容易。
什么情况下会用到这个功能?如果你要发布代码,可能需要提供一些接口,方便最终用户扩展你的代码。如果你编写了插件接口文档,他们也可以编写自己的自定义代码。
就我而言,我使用 Python 的 multiprocessing 模块来启动各种任务。核心引擎变化不大,但我编写了各种各样的任务。插件架构允许我保持引擎不变,并在插件中完成我的工作。这是一种非常好的代码拆分方式(虽然这里我不会详细介绍,但使用 Python 的异常处理机制,您可以确保即使插件出现问题,也不会导致其余代码崩溃)。
插件使用起来非常简单,我已经在各种代码中都用到了它们。让我来演示一下它们的工作原理。你真正需要的只是:(1) importlib 模块,以及 (2) 一个定义良好的接口。
在这个例子中,我将实现一个简单的计算器。加、减、乘、除运算将分别由一个插件实现(理论上我们可以扩展它的功能)。由于这只是一个示例,每个插件只会对两个数字执行相应的运算。我也省略了所有的错误检查和异常处理(例如,如果有人用字符串而不是整数调用插件,或者只传递一个参数,会发生什么情况等等)。
我创建了一个名为 /zhujimao 的目录,并在该目录下创建了一个名为 'plugins' 的目录。该目录中包含 'add.py' 插件:
#!/usr/bin/python3 类插件: def calculate(self, *args): 返回 int(args[0]) + int(args[1])
非常简单,但有两点关键:
- 插件类将与add.py、sub.py、multiply.py和divide.py中定义的类相同。稍后您就会明白这一点。
- 每个插件的 calculate() 方法签名都相同。这就是我们实际执行插件的方式。
sub.py 等程序的代码除了 calculate() 函数中的数学运算不同之外,其他部分都相同,所以我就不再赘述了。
现在让我们来看一下 calc.py,这是我们将使用这些插件的计算器。
#!/usr/bin/python3 plugin_dir = '/zhujimao/plugins' 导入 importlib、os、re、sys sys.path.append(plugin_dir) print("计算器开始。 正在加载插件:”) 插件 = {} for plugin_file in sorted(os.listdir(plugin_dir)): 如果 re.search('\.py$', plugin_file): module_name = re.sub('\.py$', '', plugin_file) module = importlib.import_module(module_name) plugins[module_name] = module.Plugin() 打印 (” %s" % (模块名称)) print("10 + 20 = %d" % plugins['add'].calculate(10,20)) print("18 - 11 = %d" % plugins['sub'].calculate(18, 11)) print("-3 * 6 = %d" % plugins['multiply'].calculate(-3, 6)) print("12 / 2 = %d" % plugins['divide'].calculate(12, 2))
我说的是“用两行代码实现”,但显然不止两行。我个人更倾向于写得更清晰明了的代码,而不是更简洁的代码,但如果你真的必须,也可以把加粗部分精简到两行:
for plugin_file in glob.glob("%s/*.py" % ( plugin_dir )): plugins[ re.sub('\.py$', '', os.path.basename(plugin_file)) ] = importlib.import_module( re.sub('\.py$', '', os.path.basename(plugin_file))).Plugin()
我折叠了一些行,让它们能放进去。但我认为原文更容易阅读!或许有更简单的方法来缩短这段文字——我以前用 Perl,所以习惯用正则表达式,但也可以考虑上下文(“with”)和 filter() 函数。
好吧,好吧,还有“import importlib”语句,以及你在插件文件里写的任何内容。我用的是标题党。来告我啊。
但我跑题了。
以下是输出结果:
计算器启动。 正在加载插件: 添加 划分 乘 子 10 + 20 = 30 18 - 11 = 7 -3 * 6 = -18 12 / 2 = 6
让我们逐条分析要点。
插件 = {}
我选择将插件放在一个字典中,格式为插件名称 -> 插件模块。
for plugin_file in sorted(os.listdir(plugin_dir)):
如果 re.search('\.py$', plugin_file):
module_name = re.sub('\.py$', '', plugin_file)
module = importlib.import_module(module_name)
plugins[module_name] = module.Plugin()
print(" %s" % ( module_name ))
这是代码的核心部分。我们检查插件目录,并对每个文件使用正则表达式(re.search)确保其以“.py”结尾(这样可以跳过 pycache 目录、旧的 vim 交换文件等)。
对于每个 .py 文件(例如 'add.py'),我们去掉“.py”后缀以获取模块名称。然后,我们将该模块导入到名为“module”的变量中。在下一行代码中,实例化该模块中的 Plugin() 类,并将其添加到我们的插件字典中。
你也可以这样做:
plugins[module_name] = importlib.import_module(module_name) adder = plugins[module_name].Plugin() sum = adder.calculte(3, 5)
在第二个例子中,你将模块存储在字典中,而在我的实现中,我们将实例化的插件对象存储在字典中。如果你想实例化插件的多个实例,你应该存储模块本身。
现在让我们来实际操作一下。对于每个操作,我们使用字典找到插件对象,然后调用它的 calculate() 方法。
print("10 + 20 = %d" % plugins['add'].calculate(10,20))
print("18 - 11 = %d" % plugins['sub'].calculate(18, 11))
print("-3 * 6 = %d" % plugins['multiply'].calculate(-3, 6))
print("12 / 2 = %d" % plugins['divide'].calculate(12, 2))
只需几行代码,我们就拥有了一个功能齐全的插件系统。不妨试一试,看看这种方法能为你的代码带来什么改变!