使用 Python 构建最小插件架构

2025-04-15 09:20:00
admin
原创
28
摘要:问题描述:我有一个用 Python 编写的应用程序,供相当技术性的受众(科学家)使用。我正在寻找一种让用户能够扩展应用程序的好方法,即脚本/插件架构。我正在寻找一些极其轻量级的东西。大多数脚本或插件不会由第三方开发、分发和安装,而是由用户在几分钟内快速完成的,用于自动执行重复任务、添加对文件格式的支持等等。因...

问题描述:

我有一个用 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

我们的用例:

插件开发团队现已与核心开发团队分离。插件开发的范围是与产品任何类别中定义的第三方应用程序集成。插件接口分为以下类别:传真、电话、电子邮件等。插件管理器可以扩展至新的类别。

就您而言:也许您可以编写一个插件并重复使用它来做事情。

如果插件开发人员需要使用重用核心对象,则可以通过在插件管理器中进行一定程度的抽象来使用该对象,以便任何插件都可以继承这些方法。

只是分享我们如何在我们的产品中实现,希望能提供一点想法。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2482  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1533  
  PLM(产品生命周期管理)项目对于企业优化产品研发流程、提升产品质量以及增强市场竞争力具有至关重要的意义。然而,在项目推进过程中,范围蔓延是一个常见且棘手的问题,它可能导致项目进度延迟、成本超支以及质量下降等一系列不良后果。因此,有效避免PLM项目范围蔓延成为项目成功的关键因素之一。以下将详细阐述三大管控策略,助力企业...
plm系统   0  
  PLM(产品生命周期管理)项目管理在企业产品研发与管理过程中扮演着至关重要的角色。随着市场竞争的加剧和产品复杂度的提升,PLM项目面临着诸多风险。准确量化风险优先级并采取有效措施应对,是确保项目成功的关键。五维评估矩阵作为一种有效的风险评估工具,能帮助项目管理者全面、系统地评估风险,为决策提供有力支持。五维评估矩阵概述...
免费plm软件   0  
  引言PLM(产品生命周期管理)开发流程对于企业产品的全生命周期管控至关重要。它涵盖了从产品概念设计到退役的各个阶段,直接影响着产品质量、开发周期以及企业的市场竞争力。在当今快速发展的科技环境下,客户对产品质量的要求日益提高,市场竞争也愈发激烈,这就使得优化PLM开发流程成为企业的必然选择。缺陷管理工具和六西格玛方法作为...
plm产品全生命周期管理   0  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用