我有一个用Python编写的应用程序,它是由相当专业的观众(科学家)使用的。

我正在寻找一个好方法,使应用程序可扩展的用户,即脚本/插件架构。

I am looking for something extremely lightweight. Most scripts, or plugins, are not going to be developed and distributed by a third-party and installed, but are going to be something whipped up by a user in a few minutes to automate a repeating task, add support for a file format, etc. So plugins should have the absolute minimum boilerplate code, and require no 'installation' other than copying to a folder (so something like setuptools entry points, or the Zope plugin architecture seems like too much.)

是否已经有类似的系统存在,或者是否有项目实现了类似的方案,我应该看看想法/灵感?


当前回答

我花了很多时间试图找到适合我需求的小型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

所以当你有根基础,用元类,并有一棵从它继承的插件树时,你可以自动获得class,它通过子类化从最特定的插件继承:

class RootExtended(RootBase.PluginExtended):
    ... your code here ...

代码库非常小(大约30行纯代码),并且继承时非常灵活。

如果你感兴趣,可以@ https://github.com/thodnev/pluginlib参与进来

其他回答

我很喜欢Andre Roberge博士在Pycon 2009上关于不同插件架构的精彩讨论。他从一些非常简单的东西开始,很好地概述了实现插件的不同方法。

它可以作为一个播客(第二部分是对猴子补丁的解释),并附有一系列的六篇博客文章。

我建议你在做决定之前先快速听一遍。

作为插件系统的另一种方法,你可以检查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]

extend_me库通过元类来操作类的创建过程,因此在上面的例子中,当创建MyCoolClass的新实例时,我们得到了一个新类的实例,它是MyCoolClassExtension和MyCoolClass的子类,由于Python的多重继承,它具有两者的功能

为了更好地控制类的创建,在这个库中定义了一些元类:

ExtensibleType—通过子类化允许简单的扩展性 ExtensibleByHashType -类似ExtensibleType,但有能力 构建类的专门版本,允许全局扩展 基类和类的专门版本扩展的

这个库是在OpenERP代理项目中使用的,似乎工作得很好!

对于实际使用的例子,看看OpenERP代理的“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

这里记录的是可执行对象。RecordDateTime是扩展名。

要启用扩展,只需导入包含扩展类的模块,并且(在上述情况下)在它之后创建的所有Record对象将在基类中具有扩展类,从而具有其所有功能。

这个库的主要优点是,操作可扩展对象的代码不需要了解扩展,而扩展可以改变可扩展对象中的所有内容。

Marty Allchin的简单插件框架是我自己使用的基础。我真的建议看看它,我认为这是一个很好的开始,如果你想要一些简单和容易破解。你也可以在Django Snippets中找到它。

当我搜索Python Decorators时,发现了一个简单但有用的代码片段。它可能不符合你的需求,但非常鼓舞人心。

Scipy高级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**")

setuptools有一个EntryPoint:

入口点是发行版“宣传”Python的一种简单方式 供其他发行版使用的对象(如函数或类)。 可扩展应用程序和框架可以搜索入口点 使用特定的名称或组,或者来自特定的分布 或者从sys。路径,然后检查或加载 可以随意发布对象。

如果你使用pip或virtualenv,这个包总是可用的。