使用 Python 构建最小插件架构
- 2025-04-15 09:20:00
- admin 原创
- 28
问题描述:
我有一个用 Python 编写的应用程序,供相当技术性的受众(科学家)使用。
我正在寻找一种让用户能够扩展应用程序的好方法,即脚本/插件架构。
我正在寻找一些极其轻量级的东西。大多数脚本或插件不会由第三方开发、分发和安装,而是由用户在几分钟内快速完成的,用于自动执行重复任务、添加对文件格式的支持等等。因此,插件应该包含绝对最少的样板代码,并且除了复制到文件夹外无需任何“安装”(因此,像 setuptools 入口点或 Zope 插件架构之类的东西似乎有点过头了。)
是否已经存在类似的系统,或者是否有实施类似方案的项目可供我参考以获取想法/灵感?
解决方案 1:
我的目录基本上是一个名为“plugins”的目录,主应用可以轮询它,然后使用imp.load_module来获取文件,查找一个可能带有模块级配置参数的已知入口点,然后从那里开始。我使用文件监控工具来获得插件处于活动状态的一定程度的动态性,但这也只是锦上添花。
当然,任何提出“我不需要[庞大复杂的东西] X;我只想要轻量级的东西”的需求,都有可能面临每次发现一个需求就重新实现 X 的风险。但这并不意味着你不能从中得到一些乐趣 :)
解决方案 2:
module_example.py
:
def plugin_main(*args, **kwargs):
print args, kwargs
loader.py
:
def load_plugin(name):
mod = __import__("module_%s" % name)
return mod
def call_plugin(name, *args, **kwargs):
plugin = load_plugin(name)
plugin.plugin_main(*args, **kwargs)
call_plugin("example", 1234)
它确实是“最小的”,它完全没有错误检查,可能存在无数的安全问题,它不是很灵活 - 但它应该向你展示 Python 中的插件系统有多么简单......
您可能也想研究一下imp__import__
模块,尽管您只需做很多事情os.listdir
和一些字符串操作即可。
解决方案 3:
看看这个关于现有插件框架/库的概述(自2009年起),它是一个很好的起点。我很喜欢yapsy,但这取决于你的用例。
解决方案 4:
虽然这个问题确实很有趣,但我认为如果没有更多细节的话很难回答。这是什么类型的应用程序?它有 GUI 吗?它是一个命令行工具吗?一组脚本?一个具有唯一入口点的程序,等等……
鉴于我掌握的信息很少,我将以非常通用的方式回答。
有什么办法可以添加插件?
您可能需要添加一个配置文件,其中将列出要加载的路径/目录。
另一种方法是说“该插件/目录中的任何文件都将被加载”,但它需要用户移动文件,这很不方便。
最后一个中间选项是要求所有插件都位于同一个插件/文件夹中,然后使用配置文件中的相对路径来激活/停用它们。
在纯粹的代码/设计实践中,你必须明确地确定你希望用户扩展哪些行为/特定操作。确定通用的入口点/一组始终会被覆盖的功能,并确定这些操作中的分组。一旦完成这些,你的应用程序应该很容易扩展。
使用钩子的示例,灵感来自 MediaWiki(PHP,但语言真的很重要吗?):
import hooks
# In your core code, on key points, you allow user to run actions:
def compute(...):
try:
hooks.runHook(hooks.registered.beforeCompute)
except hooks.hookException:
print('Error while executing plugin')
# [compute main code] ...
try:
hooks.runHook(hooks.registered.afterCompute)
except hooks.hookException:
print('Error while executing plugin')
# The idea is to insert possibilities for users to extend the behavior
# where it matters.
# If you need to, pass context parameters to runHook. Remember that
# runHook can be defined as a runHook(*args, **kwargs) function, not
# requiring you to define a common interface for *all* hooks. Quite flexible :)
# --------------------
# And in the plugin code:
# [...] plugin magic
def doStuff():
# ....
# and register the functionalities in hooks
# doStuff will be called at the end of each core.compute() call
hooks.registered.afterCompute.append(doStuff)
另一个例子,灵感来自 mercurial。在这里,扩展仅向hg命令行可执行文件添加命令,从而扩展了其行为。
def doStuff(ui, repo, *args, **kwargs):
# when called, a extension function always receives:
# * an ui object (user interface, prints, warnings, etc)
# * a repository object (main object from which most operations are doable)
# * command-line arguments that were not used by the core program
doMoreMagicStuff()
obj = maybeCreateSomeObjects()
# each extension defines a commands dictionary in the main extension file
commands = { 'newcommand': doStuff }
对于这两种方法,你的扩展程序可能需要通用的初始化和终止函数。你可以使用一个所有扩展程序都必须实现的通用接口(第二种方法更适合;Mercurial 使用了一个 reposetup(ui, repo) 函数,它会在所有扩展程序中调用),或者使用一种类似钩子的方法,即使用 hooks.setup 钩子。
但是,如果你想要更多有用的答案,你就必须缩小你的问题范围;)
解决方案 5:
Marty Allchin 的简单插件框架是我根据自身需求使用的基础。我强烈推荐大家看看这个框架,如果你想要一些简单易用的插件,它绝对是个不错的选择。你也可以在 Django Snippets 中找到它。
解决方案 6:
我是一位退休的生物学家,从事数字显微照片的研究,后来发现自己需要编写一个图像处理和分析软件包(严格来说,它不是一个库),以便在SGi机器上运行。我用C语言编写代码,并使用Tcl作为脚本语言。图形用户界面(GUI)是用Tk实现的。Tcl中出现的命令格式为“extensionName commandName arg0 arg1 ... param0 param1 ...”,即简单的空格分隔的单词和数字。当Tcl遇到“extensionName”子字符串时,控制权就交给C语言包。C语言包会将命令通过词法分析器/语法分析器(由lex/yacc完成)运行,然后根据需要调用C语言例程。
操作软件包的命令可以通过 GUI 窗口逐个运行,但批处理作业是通过编辑有效的 Tcl 脚本文本文件来完成的;您需要选择执行所需文件级操作的模板,然后编辑一个副本,使其包含实际的目录和文件名以及软件包命令。一切运行起来都很棒。直到……
1) 世界转向 PC;2) 脚本长度超过 500 行,Tcl 不稳定的组织能力开始带来真正的不便。时间流逝……
我退休了,Python 被发明出来,它看起来像是 Tcl 的完美继承者。现在,我从未进行过移植,因为我从未面对过在 PC 上编译(相当庞大的)C 程序、用 C 包扩展 Python 以及用 Python/Gt?/Tk?/?? 开发 GUI 的挑战。不过,使用可编辑模板脚本的旧想法似乎仍然可行。此外,以原生 Python 形式输入包命令应该不会太麻烦,例如:
包名称.命令(arg0,arg1,...,param0,param1,...)
一些额外的点、括号和逗号,但这些并不引人注目。
我记得看到有人用 Python 完成了 lex 和 yacc 的版本(尝试:http ://www.dabeaz.com/ply/ ),所以如果仍然需要这些,它们就在附近。
我胡扯的重点是,在我看来,Python 本身就是科学家们理想的“轻量级”前端。我很好奇你为什么认为它不是,我是认真的。
稍后添加: gedit应用程序会预估插件的添加情况,而且他们的网站对插件的简单使用流程的解释,是我花了几分钟浏览才找到的最清晰的。试试:
https://wiki.gnome.org/Apps/Gedit/PythonPluginHowToOld
我还是想更好地理解你的问题。我不清楚你是想让科学家能够以各种方式轻松使用你的(Python)应用程序,还是想让科学家能够为你的应用程序添加新功能。选项 1 是我们处理图像时遇到的情况,这导致我们使用了通用脚本,并根据当时的需求进行了修改。选项 2 是否让你想到了插件,还是你的应用程序的某些方面导致无法向其发出命令?
解决方案 7:
我在搜索 Python 装饰器时,发现了一个简单但有用的代码片段。它可能不符合你的需求,但很有启发。
Scipy Advanced Python#插件注册系统
class TextProcessor(object):
PLUGINS = []
def process(self, text, plugins=()):
if plugins is ():
for plugin in self.PLUGINS:
text = plugin().process(text)
else:
for plugin in plugins:
text = plugin().process(text)
return text
@classmethod
def plugin(cls, plugin):
cls.PLUGINS.append(plugin)
return plugin
@TextProcessor.plugin
class CleanMarkdownBolds(object):
def process(self, text):
return text.replace('**', '')
用法:
processor = TextProcessor()
processed = processor.process(text="**foo bar**", plugins=(CleanMarkdownBolds, ))
processed = processor.process(text="**foo bar**")
解决方案 8:
我很喜欢 Andre Roberge 博士在 Pycon 2009 上对不同插件架构的精彩讨论。他从一些非常简单的事情开始,很好地概述了实现插件的不同方法。
它可以作为播客(第二部分在解释 monkey-patching 之后)提供,并附带一系列六篇博客文章。
我建议您在做出决定之前先快速听一听。
解决方案 9:
我来这里是为了寻找一个极简的插件架构,却发现很多东西对我来说都显得有些多余。所以,我实现了一个超级简单的 Python 插件。要使用它,你需要创建一个或多个目录,并__init__.py
在每个目录中放入一个特殊文件。导入这些目录会导致所有其他 Python 文件作为子模块加载,并且它们的名称将被添加到__all__
列表中。然后,你需要验证/初始化/注册这些模块。README 文件中有一个示例。
解决方案 10:
实际上,setuptools与“插件目录”一起使用,如下例取自项目文档:
http://peak.telecommunity.com/DevCenter/PkgResources#locating-plugins
使用示例:
plugin_dirs = ['foo/plugins'] + sys.path
env = Environment(plugin_dirs)
distributions, errors = working_set.find_plugins(env)
map(working_set.add, distributions) # add plugins+libs to sys.path
print("Couldn't load plugins due to: %s" % errors)
从长远来看,setuptools是一个更安全的选择,因为它可以加载插件而不会发生冲突或缺少要求。
另一个好处是插件本身可以使用相同的机制进行扩展,而无需原始应用程序关心它。
解决方案 11:
扩展@edomaur 的答案,我建议看一下simple_plugins(无耻的插件),这是一个受Marty Alchin 的作品启发的简单插件框架。
基于项目 README 的简短使用示例:
# All plugin info
>>> BaseHttpResponse.plugins.keys()
['valid_ids', 'instances_sorted_by_id', 'id_to_class', 'instances',
'classes', 'class_to_id', 'id_to_instance']
# Plugin info can be accessed using either dict...
>>> BaseHttpResponse.plugins['valid_ids']
set([304, 400, 404, 200, 301])
# ... or object notation
>>> BaseHttpResponse.plugins.valid_ids
set([304, 400, 404, 200, 301])
>>> BaseHttpResponse.plugins.classes
set([<class '__main__.NotFound'>, <class '__main__.OK'>,
<class '__main__.NotModified'>, <class '__main__.BadRequest'>,
<class '__main__.MovedPermanently'>])
>>> BaseHttpResponse.plugins.id_to_class[200]
<class '__main__.OK'>
>>> BaseHttpResponse.plugins.id_to_instance[200]
<OK: 200>
>>> BaseHttpResponse.plugins.instances_sorted_by_id
[<OK: 200>, <MovedPermanently: 301>, <NotModified: 304>, <BadRequest: 400>, <NotFound: 404>]
# Coerce the passed value into the right instance
>>> BaseHttpResponse.coerce(200)
<OK: 200>
解决方案 12:
作为插件系统的另一种方法,您可以检查Extend Me 项目。
例如,我们定义简单类及其扩展
# Define base class for extensions (mount point)
class MyCoolClass(Extensible):
my_attr_1 = 25
def my_method1(self, arg1):
print('Hello, %s' % arg1)
# Define extension, which implements some aditional logic
# or modifies existing logic of base class (MyCoolClass)
# Also any extension class maby be placed in any module You like,
# It just needs to be imported at start of app
class MyCoolClassExtension1(MyCoolClass):
def my_method1(self, arg1):
super(MyCoolClassExtension1, self).my_method1(arg1.upper())
def my_method2(self, arg1):
print("Good by, %s" % arg1)
并尝试使用它:
>>> my_cool_obj = MyCoolClass()
>>> print(my_cool_obj.my_attr_1)
25
>>> my_cool_obj.my_method1('World')
Hello, WORLD
>>> my_cool_obj.my_method2('World')
Good by, World
并展示幕后隐藏的内容:
>>> my_cool_obj.__class__.__bases__
[MyCoolClassExtension1, MyCoolClass]
extends_me库通过元类来操作类的创建过程,因此在上面的例子中,当创建新的实例时,MyCoolClass
我们得到了一个新类的实例,它是两者的子类MyCoolClassExtension
,并且MyCoolClass
具有两者的功能,这要归功于 Python 的多重继承
为了更好地控制类的创建,这个库中定义了一些元类:
ExtensibleType
- 允许通过子类化实现简单的扩展ExtensibleByHashType
- 类似于 ExtensibleType,但具有构建类的专门版本的能力,允许基类的全局扩展和类的专门版本的扩展
这个库在OpenERP 代理项目中使用,并且似乎运行得足够好!
有关实际使用示例,请查看OpenERP Proxy‘field_datetime’扩展:
from ..orm.record import Record
import datetime
class RecordDateTime(Record):
""" Provides auto conversion of datetime fields from
string got from server to comparable datetime objects
"""
def _get_field(self, ftype, name):
res = super(RecordDateTime, self)._get_field(ftype, name)
if res and ftype == 'date':
return datetime.datetime.strptime(res, '%Y-%m-%d').date()
elif res and ftype == 'datetime':
return datetime.datetime.strptime(res, '%Y-%m-%d %H:%M:%S')
return res
Record
这里是可扩展对象。RecordDateTime
是扩展。
要启用扩展,只需导入包含扩展类的模块,并且(在上述情况下)Record
在它之后创建的所有对象都将在基类中具有扩展类,从而具有其所有功能。
这个库的主要优点是,操作可扩展对象的代码不需要了解扩展,并且扩展可以改变可扩展对象中的一切。
解决方案 13:
setuptools 有一个 EntryPoint:
入口点是发行版向其他发行版“宣传” Python 对象(例如函数或类)的一种简单方法。可扩展的应用程序和框架可以从特定发行版或 sys.path 上所有活跃的发行版中搜索具有特定名称或组的入口点,然后根据需要检查或加载这些宣传的对象。
据我所知,如果您使用 pip 或 virtualenv,此包始终可用。
解决方案 14:
您可以使用pluginlib。
插件很容易创建,可以从其他包、文件路径或入口点加载。
创建插件父类,定义任何所需的方法:
import pluginlib
@pluginlib.Parent('parser')
class Parser(object):
@pluginlib.abstractmethod
def parse(self, string):
pass
通过继承父类来创建插件:
import json
class JSON(Parser):
_alias_ = 'json'
def parse(self, string):
return json.loads(string)
加载插件:
loader = pluginlib.PluginLoader(modules=['sample_plugins'])
plugins = loader.plugins
parser = plugins.parser.json()
print(parser.parse('{"json": "test"}'))
解决方案 15:
我在搜索 Python 插件框架的时候,也花了一些时间阅读这个帖子。我用过一些,但它们都有一些缺点。这是我在 2017 年为大家精心打造的,一个无接口、松耦合的插件管理系统:Load me later。这里有一些使用教程。
解决方案 16:
我花了很多时间尝试寻找一个能满足我需求的 Python 小型插件系统。但后来我突然想到,既然已经有了继承,既自然又灵活,为什么不利用它呢?
使用插件继承的唯一问题是您不知道最具体的(继承树上最低的)插件类是什么。
但这可以通过元类来解决,元类可以跟踪基类的继承,并且可以构建从大多数特定插件继承的类(下图中的“根扩展”)
因此我通过编写这样的元类找到了解决方案:
class PluginBaseMeta(type):
def __new__(mcls, name, bases, namespace):
cls = super(PluginBaseMeta, mcls).__new__(mcls, name, bases, namespace)
if not hasattr(cls, '__pluginextensions__'): # parent class
cls.__pluginextensions__ = {cls} # set reflects lowest plugins
cls.__pluginroot__ = cls
cls.__pluginiscachevalid__ = False
else: # subclass
assert not set(namespace) & {'__pluginextensions__',
'__pluginroot__'} # only in parent
exts = cls.__pluginextensions__
exts.difference_update(set(bases)) # remove parents
exts.add(cls) # and add current
cls.__pluginroot__.__pluginiscachevalid__ = False
return cls
@property
def PluginExtended(cls):
# After PluginExtended creation we'll have only 1 item in set
# so this is used for caching, mainly not to create same PluginExtended
if cls.__pluginroot__.__pluginiscachevalid__:
return next(iter(cls.__pluginextensions__)) # only 1 item in set
else:
name = cls.__pluginroot__.__name__ + 'PluginExtended'
extended = type(name, tuple(cls.__pluginextensions__), {})
cls.__pluginroot__.__pluginiscachevalid__ = True
return extended
因此,当您拥有由元类构成的 Root 基类,并且拥有从它继承的插件树时,您可以通过子类化自动获取从最具体的插件继承的类:
class RootExtended(RootBase.PluginExtended):
... your code here ...
代码库非常小(~30 行纯代码)并且与继承允许的一样灵活。
如果您有兴趣,请参与@ https://github.com/thodnev/pluginlib
解决方案 17:
您还可以查看Groundwork。
其理念是围绕可重用组件(称为模式和插件)构建应用程序。插件是派生自的类GwBasePattern
。以下是一个简单的示例:
from groundwork import App
from groundwork.patterns import GwBasePattern
class MyPlugin(GwBasePattern):
def __init__(self, app, **kwargs):
self.name = "My Plugin"
super().__init__(app, **kwargs)
def activate(self):
pass
def deactivate(self):
pass
my_app = App(plugins=[MyPlugin]) # register plugin
my_app.plugins.activate(["My Plugin"]) # activate it
还有更高级的模式来处理例如命令行界面、信令或共享对象。
Groundwork 可以通过编程方式将插件绑定到应用程序(如上所示)或通过自动方式找到插件setuptools
。包含插件的 Python 包必须使用特殊的入口点声明这些插件groundwork.plugin
。
这是文档。
免责声明:我是 Groundwork 的作者之一。
解决方案 18:
我们目前的医疗保健产品采用的是一个通过接口类实现的插件架构。我们的技术栈是基于 Python 的 Django 开发 API,以及基于 Node.js 的 Nuxtjs 开发前端。
我们为我们的产品编写了一个插件管理器应用程序,它基本上是符合 Django 和 Nuxtjs 的 pip 和 npm 包。
对于新插件开发(pip 和 npm),我们将插件管理器作为依赖项。
在 Pip 包中:借助 setup.py,您可以添加插件的入口点来使用插件管理器执行某些操作(注册表、启动等)
https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation
在 npm 包中:与 pip 类似,npm 脚本中有一些钩子来处理安装。https
://docs.npmjs.com/misc/scripts
我们的用例:
插件开发团队现已与核心开发团队分离。插件开发的范围是与产品任何类别中定义的第三方应用程序集成。插件接口分为以下类别:传真、电话、电子邮件等。插件管理器可以扩展至新的类别。
就您而言:也许您可以编写一个插件并重复使用它来做事情。
如果插件开发人员需要使用重用核心对象,则可以通过在插件管理器中进行一定程度的抽象来使用该对象,以便任何插件都可以继承这些方法。
只是分享我们如何在我们的产品中实现,希望能提供一点想法。
扫码咨询,免费领取项目管理大礼包!