Python 插件系统 - 如何实现

2 投票
4 回答
1836 浏览
提问于 2025-04-16 03:49

我正在写一个Python应用程序,用来存储一些数据。为了存储这些数据,我创建了一个叫做Connection的类,这个类里面有一些抽象方法(使用Python的abc模块)。这个类是所有存储后端的父类,所有的存储后端都是从这个类派生出来的。每个存储后端只有一个目的,比如把数据存储在普通的文本文件里或者XML文件里。

所有的存储后端(包括父类所在的模块)都在一个叫做'data_handler'的包里。每个后端都在一个单独的模块中。

我的应用程序应该能够同时在多个后端存储数据,并且在运行时确定哪些存储后端是可用的。为此,我想到了写一个单例类,让每个后端在导入时进行注册。但我觉得在动态语言中这样做可能不是很好(如果我理解错了,请纠正我)。另一种方法是导入这个包,使用import data_handler,然后获取这个包的__file__属性,搜索目录下所有的Python文件,找出哪些是Connection类的子类。

我应该使用哪种方法,或者有没有其他(可能更好的)方法来实现这个?

Stefan


在运行时发现后端是一个严格的要求,还是在代码中静态列举它们就可以了?

这个功能很好,这样我在添加新的后端时就不需要修改代码了。


但是你的应用程序是否总是需要写入所有后端?

我会有一个类,可以在其中注册可用的处理器。数据将写入每个注册的处理器。但并不是所有可用的处理器都必须注册。

4 个回答

0

你可以使用这样的一个函数:

def loadClass(fullclassname):
    sepindex=fullclassname.rindex('.')    
    classname=fullclassname[sepindex+1:]
    modname=fullclassname[:sepindex]
    #dynmically import the class in the module
    imod=__import__(modname,None,None,classname)
    classtype=getattr(imod,classname)
    return classtype

这里的fullclassname是你想要加载的类的完整名称,通常是用点号分隔的。

举个例子(伪代码,但思路是这样的):

在检查包是否可用时,只需要进行一些简单的匹配,然后为了找到最终的类名,你可以在每个模块中声明一个Plugin类,并且这个类里有一个getStorage()的方法。

 #scan for modules , getPluginPackagesUnder (to be defined) returns the dotted name for all packages under a root path (using some globbing, listdir or whatever method) 

    pluginpackages=getPluginPackagesUnder("x/y/z")
    storagelist=[]
    for pgpck in plunginpackages:
        pluginclass=loadClass("%s.Plugin"%pgpck)
        storageinstance=Plugin().getStorage()
        storagelist.append(storageinstance)

这样,你就可以动态地扫描你现有的存储插件了。

1

如果这些后端要在不同的Python版本中分发,你可能需要了解一下setuptools或distribute的入口点。这里有一篇文章,介绍了如何利用这些工具来动态寻找插件:

http://aroberge.blogspot.com/2008/12/plugins-part-6-setuptools-based.html

3

千万不要去遍历文件系统,去扫描后端的Python源代码!这是一种很糟糕的做法,尤其在这里根本不需要这样做!在导入时注册所有的类是完全可以的。


把后端存储在类属性中,而不是实例属性中;这样,所有的Storage实例就会查看同一组后端:

>>> class Storage(object):
...     backends = set()
...
...     def register(self, backend):
...             self.backends.add(backend)
...

每个后端可以通过实例化自己的Storage来注册自己,这样它就可以访问类级别的backends属性:

>>> foo = Storage()
>>> foo.register("text")
>>> bar = Storage()
>>> bar.register("xml")

你可以通过实例化另一个Storage来读取这个属性,这样它会读取到同一个变量:

>>> baz = Storage()
>>> baz.backends
{'xml', 'text'}

你甚至可以把后端实例存储在Connection的类属性中,并在实例化时注册每个后端:

>>> class Connection(object,metaclass=abc.ABCMeta):
...     @abc.abstractmethod
...     def register(self, backend):
...             pass
...
...     backends = set()
...
>>> class TextBackend(Connection):
...     def register(self):
...             super().backends.add(self)
...
...     def __init__(self):
...             self.register()
...
>>> class XMLBackend(Connection):
...     def register(self):
...             super().backends.add(self)
...
...     def __init__(self):
...             self.register()
...
>>> foo = TextBackend()
>>> bar = XMLBackend()
>>> Connection.backends
{<__main__.XMLBackend object at 0x027ADAB0>, \
<__main__.TextBackend object at 0x027ADA50>}

撰写回答