基於Python的插件式系統結構試驗


因爲Python支持運行時動態載入,設計一個插件式結構是比較簡單的。若是使用PyQt的話,能夠輕鬆地建立出一個插件式的UI結構。不過,在不少時候,主程序使用C++/STL編寫,經過Python來實現插件擴展。這裏主要探討「純Python」實現的插件結構。C++Python的模式後面再說(可參考,C++嵌入Python: http://my.oschina.net/u/2306127/blog/370001)。javascript

若是要經過C++編寫一個Python模塊進行加載,能夠參考:http://my.oschina.net/u/2306127/blog/369997java

爲了擴充軟件的功能,一般咱們會把軟件設計成插件式結構。Python這樣的動態語言天生就支持插件式編程。與C++相比,Python已經定義好模塊的接口,想要載入一個插件,一個__import__()就能很輕鬆地搞定。不須要特定的底層知識。並且與C++等靜態語言相比,Python的插件式結構更顯靈活。由於插件載入後,能夠利用Python語言的動態性,充分地修改核心的邏輯。程序員

簡單地說一個__import__()可能不大清楚。如今就來看一個最簡單的插件式結構程序。它會掃描plugins文件夾下的全部.py文件。而後把它們載入。編程

#-*- encoding: utf-8 -*-#main1.pyimport osclass Platform:
    def __init__(self):
        self.loadPlugins()

    def sayHello(self, from_):
        print "hello from %s." % from_

    def loadPlugins(self):
        for filename in os.listdir("plugins"):
            if not filename.endswith(".py") or filename.startswith("_"):
                continue
            self.runPlugin(filename)

    def runPlugin(self, filename):
        pluginName=os.path.splitext(filename)[0]
        plugin=__import__("plugins."+pluginName, fromlist=[pluginName])
        #Errors may be occured. Handle it yourself.
        plugin.run(self)if __name__=="__main__":
    platform=Platform()

而後在plugins子目錄裏面放入兩個文件:app

#plugins1.pydef run(platform):
    platform.sayHello("plugin1")#plugins2.pydef run(platform):
    platform.sayHello("plugin2")

再建立一個空的__init__.pyplugins文件夾裏面。從package裏面導入模塊的時候,Python要求一個__init__.py函數

運行main1.py,看一下運行的結果。首先是打印一下文件夾結構方便你們理解:網站

h:\projects\workon\testplugins>tree /f /a
卷 Data 的文件夾 PATH 列表
卷序列號爲 ****-****
H:.
|   main1.py
|
\---plugins
        plugin1.py
        plugin2.py
        __init__.py

h:\projects\workon\testplugins>main1.py
hello from plugin1.
hello from plugin2.

通常地,載入插件前要首先掃描插件,而後依次載入並運行插件。咱們上面的示例程序main1.py也是如此,分爲兩個函數。第一個loadPlugins()掃描插件。它把plugins目錄下面全部.py的文件除了__init__.py都當成插件。runPlugin()載入並運行插件。其中兩個關鍵:使用__import__()函數把插件當成模塊導入,它要求全部的插件都定義一個run()函數。各類語言實現的插件式結構其實也基本上分爲這兩個步驟。所不一樣的是,Python語言實現起來更加的簡潔。.net

或許聽起來還有點玄奧。詳細地說一下__import__()。它和常見的import語句很類似,只不過換成函數形式而且返回模塊以供調用。import module至關於__import__("module")from module import func至關於__import__("module", fromlist=["func"]),不過與想象有點不一樣,import package.module至關於__import__("package.module", fromlist=["module"])插件

如何調用插件通常有個約定。像咱們這裏就約定每一個插件都實現一個run()。有時候還能夠約定實現一個類,而且要求這個類實現某個管理接口,以方便核心隨時啓動、中止插件。要求全部的插件都有這幾個接口方法:設計

#interfaces.pyclass Plugin:
    def setPlatform(self, platform):
        self.platform=platform

    def start(self):
        pass

    def stop(self):
        pass

想要運行這個插件,咱們的runPlugin()要改一改,另外增長一個shutdown()來中止插件:

class Platform:
    def __init__(self):
        self.plugins=[]
        self.loadPlugins()

    def sayHello(self, from_):
        print "hello from %s." % from_

    def loadPlugins(self):
        for filename in os.listdir("plugins"):
            if not filename.endswith(".py") or filename.startswith("_"):
                continue
            self.runPlugin(filename)

    def runPlugin(self, filename):
        pluginName=os.path.splitext(filename)[0]
        plugin=__import__("plugins."+pluginName, fromlist=[pluginName])
        clazz=plugin.getPluginClass()
        o=clazz()
        o.setPlatform(self)
        o.start()
        self.plugins.append(o)

    def shutdown(self):
        for o in self.plugins:
            o.stop()
            o.setPlatform(None)
        self.plugins=[]if __name__=="__main__":
    platform=Platform()
    platform.shutdown()

插件改爲這樣:

#plugins1.pyclass Plugin1:
    def setPlatform(self, platform):
        self.platform=platform

    def start(self):
        self.platform.sayHello("plugin1")

    def stop(self):
        self.platform.sayGoodbye("plugin1")def getPluginClass():
    return Plugin1#plugins2.pydef sayGoodbye(self, from_):
    print "goodbye from %s." % from_class Plugin2:
    def setPlatform(self, platform):
        self.platform=platform
        if platform is not None:
            platform.__class__.sayGoodbye=sayGoodbye

    def start(self):
        self.platform.sayHello("plugin2")

    def stop(self):
        self.platform.sayGoodbye("plugin2")def getPluginClass():
    return Plugin2

運行結果:

h:\projects\workon\testplugins>main.py
hello from plugin1.
hello from plugin2.
goodbye from plugin1.
goodbye from plugin2.

詳細觀察的朋友們可能會發現,上面的main.py,plugin1.py, plugin2.py幹了好幾件使人驚奇的事。

首先,plugin1.pyplugin2.py裏面的插件類並無繼承自interfaces.Plugin,而platform仍然能夠直接調用它們的start()stop()方法。這件事在Java、C++裏面多是件麻煩的事情,可是在Python裏面倒是件稀疏日常的事,彷彿吃飯喝水通常正常。事實上,這正是Python鼓勵的約定編程。Python的文件接口協議就只規定了read(), write(), close()少數幾個方法。多數以文件做爲參數的函數均可以傳入自定義的文件對象,只要實現其中一兩個方法就好了,而沒必要實現一個什麼FileInterface。若是那樣的話,須要實現的函數就多了,可能要有十幾個。

再仔細看下來,getPluginClass()能夠把類型當成值返回。其實不止是類型,Python的函數、模塊均可以被當成普通的對象使用。從類型生成一個實例也很簡單,直接調用clazz()就建立一個對象。不只如此,Python還可以修改類型。上面的例子咱們就演示瞭如何給Platform增長一個方法。在兩個插件的stop()裏面咱們都調用了sayGoodbye(),可是仔細觀察Platform的定義,裏面並無定義。原理就在這裏:

#plugins2.pydef sayGoodbye(self, from_):
    print "goodbye from %s." % from_class Plugin2:
    def setPlatform(self, platform):
        self.platform=platform
        if platform is not None:
            platform.__class__.sayGoodbye=sayGoodbye

這裏首先經過platform.__class__獲得Platform類型,而後Platform.sayGoodbye=sayGoodbye新 增了一個方法。使用這種方法,咱們可讓插件任意修改核心的邏輯。這正在文首所說的Python實現插件式結構的靈活性,是靜態語言如C++、Java等 沒法比擬的。固然,這只是演示,我不大建議使用這種方式,它改變了核心的API,可能會給其它程序員形成困惑。可是能夠採用這種方式替換原來的方法,還可 以利用「面向切面編程」,加強系統的功能。

接下來咱們還要再改進一下載入插件的方法,或者說插件的佈署方法。前面咱們實現的插件體系主要的缺點是每一個插件只能有一個源代碼。若是想附帶一些圖 片、聲音數據,又怕它們會和其它的插件衝突。即便不衝突,下載時分紅單獨的文件也不方便。最好是把一個插件壓縮成一個文件供下載安裝。

Firefox是一個支持插件的著名軟件。它的插件以.xpi做爲擴展名,其實是一個.zip文件,裏面包含了javascript代碼、數據文件等不少內容。它會把插件包下載複製並解壓到%APPDATA%\Mozilla\Firefox\Profiles\XXXX.default\extensions裏面,而後調用其中的install.js安裝。與此相似,實用的Python程序也不大可能只有一個源代碼,也要像Firefox那樣支持.zip包格式。

實現一個相似於Firefox那樣的插件佈署體系並不會很難,由於Python支持讀寫.zip文件,只要寫幾行代碼來作壓縮與解壓縮就好了。首先要看一下zipfile這個模塊。用它解壓縮的代碼以下:

import zipfile, osdef installPlugin(filename):
    with zipfile.ZipFile(filename) as pluginzip:
        subdir=os.path.splitext(filename)[0]
        topath=os.path.join("plugins", subdir)
        pluginzip.extractall(topath)

ZipFile.extractall()是Python 2.6後新增的函數。它直接解壓全部壓縮包內的文件。不過這個函數只能用於受信任的壓縮包。若是壓縮包內包含了以/或者盤符開始的絕對路徑,頗有可能會損壞系統。推薦看一下zipfile模塊的說明文檔,事先過濾非法的路徑名。

這裏只有解壓縮的一小段代碼,安裝過程的界面交互相關的代碼不少,不可能在這裏舉例說明。我以爲UI是很是考驗軟件設計師的部分。常見的軟件會要求 用戶到網站上查找並下載插件。而Firefox和KDE提供了一個「組件(部件)管理界面」,用戶能夠直接在界面內查找插件,查看它的描述,而後直接點擊 安裝。安裝後,咱們的程序遍歷插件目錄,載入全部的插件。通常地,軟件還須要向用戶提供插件的啓用、禁用、依賴等功能,甚至可讓用戶直接在軟件界面上給 插件評分,這裏就再也不詳述了。

有個小技巧,安裝到plugins/subdir下的插件能夠經過__file__獲得它本身的絕對路徑。若是這個插件帶有圖片、聲音等數據的時候,能夠利用這個功能載入它們。好比上面的plugin1.py這個插件,若是它想在啓動的時候播放同目錄的message.wav,能夠這樣子:

#plugins1.pyimport osdef alert():
    soundFile=os.path.join(os.path.dirname(__file__), "message.wav")
    try:
        import winsound
        winsound.PlaySound(soundFile, winsound.SND_FILENAME)
    except (ImportError, RuntimeError):
        passclass Plugin1:
    def setPlatform(self, platform):
        self.platform=platform

    def start(self):
        self.platform.sayHello("plugin1")
        alert()

    def stop(self):
        self.platform.sayGoodbye("plugin1")def getPluginClass():
    return Plugin1

接下來咱們再介紹一種Python/Java語言經常使用的插件管理方式。它不須要事先有一個插件解壓過程,由於Python支持從.zp文件導入模塊,很相似於Java直接從.jar文件載入代碼。所謂安裝,只要簡單地把插件複製到特定的目錄便可,Python代碼自動掃描並從.zip文件內載入代碼。下面是一個最簡單的例子,它和上面的幾個例子同樣,包含一個main.py,這是主程序,一個plugins子目錄,用於存放插件。咱們這裏只有一個插件,名爲plugin1.zipplugin1.zip有如下兩個文件,其中description.txt保存了插件內的入口函數和插件的名字等信息,而plugin1.py是插件的主要代碼:

description.txt
plugin1.py

其中description.txt的內容是:

[general]name=plugin1description=Just a test code=plugin1.Plugin1

plugin1.py與前面的例子相似,爲了省事,咱們去掉了stop()方法,它的內容是:

class Plugin1:
    def setPlatform(self, platform):
        self.platform=platform

    def start(self):
        self.platform.sayHello("plugin1")

重寫的main.py的內容是:

# -*- coding: utf-8 -*-import os, zipfile, sys, ConfigParserclass Platform:
    def __init__(self):
        self.loadPlugins()

    def sayHello(self, from_):
        print "hello from %s." % from_

    def loadPlugins(self):
        for filename in os.listdir("plugins"):
            if not filename.endswith(".zip"):
                continue
            self.runPlugin(filename)

    def runPlugin(self, filename):
        pluginPath=os.path.join("plugins", filename)
        pluginInfo, plugin = self.getPlugin(pluginPath)
        print "loading plugin: %s, description: %s" % \                (pluginInfo["name"], pluginInfo["description"])
        plugin.setPlatform(self)
        plugin.start()

    def getPlugin(self, pluginPath):
        pluginzip=zipfile.ZipFile(pluginPath, "r")
        description_txt=pluginzip.open("description.txt")
        parser=ConfigParser.ConfigParser()
        parser.readfp(description_txt)
        pluginInfo={}
        pluginInfo["name"]=parser.get("general", "name")
        pluginInfo["description"]=parser.get("general", "description")
        pluginInfo["code"]=parser.get("general", "code")

        sys.path.append(pluginPath)
        moduleName, pluginClassName=pluginInfo["code"].rsplit(".", 1)
        module=__import__(moduleName, fromlist=[pluginClassName, ])
        pluginClass=getattr(module, pluginClassName)
        plugin=pluginClass()
        return pluginInfo, pluginif __name__=="__main__":
    platform=Platform()

與前一個例子的主要不一樣之處是getPlugin()。它首先從.zip文件內讀取描述信息,而後把這個.zip文件添加到sys.path裏面。最後與前面相似地導入模塊並執行。

解壓仍是不解壓,兩種方案各有優劣。通常地,把.zip文件解壓到獨立的文件夾內須要一個解壓縮過程,或者是人工解壓,或者是由軟件解壓。解壓後的運行效率會高一些。而直接使用.zip包的話,只須要讓用戶把插件複製到特定的位置便可,可是每次運行的時候都須要在內存裏面解壓縮,效率下降。另外,從.zip文件讀取數據老是比較麻煩。推薦不包含沒有數據文件的時候使用。

相關文章
相關標籤/搜索