Mix-in技術與分布類編程
作者:limodou
大家一看到這個題目,看到“分布類編程”可能會認為是一種什麼新技術,其實只不過是我個人所創,是指一個類的實現由多個文件(或模塊)組成。至於它如何構成,有何作用,及相應的實例且聽我慢慢道來。
Mix-in技術簡介
關於Mix-in技術本人有專門的文章講述,這裡就不再贅述,而只進行簡單地介紹。[1]
如果我們在運行時改變一個類的基類或類的方法、屬性等就叫做Mix-in。那麼它與類派生和重載有什麼區別呢?根本的區別就是它的動態性。派生和重載是在程序運行前就已經對類進行了修改,它的改變是確切存在於某個文件中的,這種改變在運行時是穩定的。而Mix-in是在運行時才對相應的類發生作用,其運行時的表現與文件中所描述可能有所區別,而且隨著運行環境的不同其表現也可以發生變化。另外,對於派生後的類,在運行時創建其一個實例就可以使用了。而使用Mix-in技術,我們需要將新的基類或方法先加入到原來的類中,然後再創建實例進行應用。
Mix-in技術的主要實現方法有兩種:加入基類和加入方法。加入基類則相當於從基類進行派生,從而使原來的類具有基類的方法和屬性。加入方法使來的類具有新的方法,如果存在加入的方法與原類中的某個方法同名,則新的方法將替換原來的方法(這個規則不是必然的,因為Mix-in的實現是你自已編寫的,而不是固定不變的)。加入基類可以修改類的__bases__屬性。加入方法可以使用內置函數getattr()和setattr()來實現。那麼對於這種Mix-in技術在實現時還要考慮當新的方法與原方法重名的處理。
之所以會有這種偉大的Mix-in技術的存在,完全要得益於Python語言的動態性(當然可能不止一種語言能夠實現這種技術),它允許你在運行時修改類所有的屬性,也可以增加新的屬性。通過setattr()我們就可以向一個類增加新的方法,並且使用它。
分布類的構成
類是對象的定義和描述,通過類我們就可以生成實例,即對象,用它進行處理。分布類,即表明類的定義不是一次性定義出來,而是分布在不同的文件或模塊(以後我們就叫它們為分布類文件或分布類模塊)中定義的。可能的一種類結構的分布如下:
+----原始類
+----基類1
+...
+----基類n
+----新功能1
+...
+----新功能2
+----Mix-in模塊
上面的意思就是在一個目錄下,有一個原始類,可能有n個基類模塊,有n個新功能模塊。最後為了實現Mix-in技術還需要一個Mix-in模塊,而這個模塊可以是你自已編寫的。更復雜的結構可能是原始類下還有子目錄,但處理方法都是一樣的。
在進行基類與新功能的編程時要考慮重名方法的情況。在新功能模塊中,方法定義應按照類的方法進行定義,即第一個參數應為對象實例參數,如self。
在運行時,應先使用Mix-in模塊中的處理函數將基類與新功能加入到原始類中,然後再生成類的實例,進行使用。
大家可以看到,將類分成多個文件進行編程並不復雜,而Mix-in的處理是比較復雜的,應根據實際情況進行處理。
分布類的作用
為什麼要使用分布類呢?就我個人來說,主要有以下的好處:
清晰。把一個類的不同功能分組放在不同的模塊中,可以清晰地看出類的構成,容易理解,維護起來相對容易。我們在寫程序時,可以將最核心的功能寫在一個文件中,在其中定義類的屬性,類的初始化,及一些最基本的功能。然後對於類的擴展,我們可以根據功能分組放到不同的文件中。這樣使得每個文件所描述的功能相對集中,而不再是擁腫的代碼。
靈活。如果我們的類的功能耦合性非常小,那麼增加、卸載功能都是相當的容易,只要把相應的文件加入或去掉就可以了。在調試程序和定位錯誤時可能會很方便。如果我們想使某些功能有效,就加入相應的文件,反之,去掉相應的文件。如果發現有錯誤,可以通過一個文件一個文件地減少,從而進行一個文件一個文件地進行排除。
那麼可能帶來的不便是:文件變多,沒有一個完整的靜態描述。那麼如果喜歡靜態類,我們完全可以在類的功能測試完全之後再合並成靜態類。至於需不需要這樣做完全看你自已了。
使用實例
下面舉一個本人所寫的FlyEdit程序中所用到的例子。FlyEdit是一個編輯器,它的基本結構就是建立在分布類的基礎之上,其中還有一些特別的處理。FlyEdit的分布類模塊放在兩個目錄下,一個為modules,另一個在plugin中。modules為相對核心的部分;plugin中為可由用戶自行編寫的插件功能模塊。在FlyEdit中,最主要的一個類是編輯器類(Editor),它實現了編輯器的基本功能,同時規定了分布類模塊某些功能的執行位置(如分布類模塊的初始化就放在Editor類的初始化函數內,而且放在所有的可視控件初始化完成之後)。這個類放在主程序 flyedit.pyw中。同時flyedit.pyw中在生成編輯器類時先進行Mix-in的處理,然後再生成類的實例。
1 import register
2 register.registerpackage(Editor, 'modules')
3 register.registerpackage(Editor, 'plugin')
4 root=Tk()
5
6 from windowlist import windowlist
7 windowlist=windowlist(root)
8 Editor(root, file, windowlist)
9 root.mainloop()
第1行導入register模塊。它是我寫的實現Mix-in功能的模塊。
第2,3行,對modules子目錄和plugin子目錄下的文件進行Mix-in處理,將新功能加到Editor類上。在分布類模塊中不僅有新的方法,還有一些新增的數據,如菜單等。這樣為了使分布類的加入順序不變,將modules和plugin目錄做成python包的形式進行處理(即加入 __init__.py文件);同時在包的__init__.py文件中定義了__all__列表,用來說明哪些模塊應該Mix-in,並且加入的順序如何。
第8行,使用新的類創建實例。
下面舉一個這樣的包結構。
+----modules
+----__init__.py
+----a.py
+----b.py
對於__init__.py文件,內容可能為:
__all__=['a', 'b']
在modules目錄下共有三個文件,其中__init__.py文件記錄了哪些模塊將Mix-in到待處理的類中去。它通過定義一個全局變量 __all__來實現。__all__為一個列表,每一項記錄了要Mix-in到類中去的模塊文件名(沒有.py的後綴);同時列表的順序表明每個模塊 Mix-in的順序。a.py和b.py即為真正的分布類模塊。從__all__的內容上看,先處理a.py再處理b.py。如果想改變處理順序或去掉哪些模塊,則只要修改__all__的列表值即可,而不用將相應的文件刪除。
對於分布類模塊,如a.py中應定義要Mix-in到類中去的方法或屬性。同時也應定義一個__all__列表,其中列出所有要放入到類中去的方法、屬性的字符串名稱。那麼在進行Mix-in處理時,只有在__all__中定義的方法或屬性才會被處理。
那麼Mix-in過程是如何實現的呢?下面將FlyEdit中所用到的register模塊簡述一下。
01 #register functions to a class
02 def registerpackage(class_, modulename):
03 module=__import__(modulename)
04 if hasattr(module, '__all__'):
05 for i in module.__all__:
06 register(class_, modulename+'.'+i)
07
08 def register(class_, modulename):
09 import types
10 module=my_import(modulename)
11 if hasattr(module, 'init'):
12 value=getattr(module, 'init')
13 class_.initlist.append(value)
14 if not hasattr(module, '__all__'): return
15 for name in module.__all__:
16 property=getattr(module, name)
17 t=type(property)
18 if t in (types.DictType, types.TupleType, types.ListType):
19 if hasattr(class_, name):
20 value=getattr(class_, name)
21 if t == types.DictType :
22 value.update(property)
23 else:
24 value=value+property
25 else:
26 value=property
27 setattr(class_, name, value)
28 else:
29 if hasattr(class_, name): #exist a func
30 delattr(class_, name)
31 setattr(class_, name, property)
32
33 def my_import(name):
34 mod = __import__(name)
35 components = name.split('.')
36 for comp in components[1:]:
37 mod = getattr(mod, comp)
38 return mod
02行,定義函數registerpackage,用於將一個包結構Mix-in到類上。
03行,導入包。由於這裡使用字符串名字作為包名,故應使用__import__內置函數。
4行到6行,判斷如果包中含有__all__屬性(變量),則依次取出__all__的值(每個值為一個模塊名),然後把每個模塊名與包名相接,生成“包名.模塊名”的可調用的形式,再調用register函數Mix-in到類中。
08行,定義將某個模塊Mix-in到指定類的函數。
10行,導入真正的模塊。使用__import__函數時,對形如“包名.模塊名”的模塊,只會返回包對象,而不是最後的模塊對象。故調用一個可以真正返回模塊對象的函數my_import。
11行到13行,判斷模塊是否有init屬性,如果有則取出放到類的初始化列表中(這個處理是針對FlyEdit所特有的,主要是想解決某些模塊需初始的問題,在你的應用中可以不使用)。
14行,判斷模塊如果沒有__all__屬性,則不會進行Mix-in處理。這樣,只有定義在__all__列表中的內容才會被Mix-in,而其它的內容會被看到“私有”內容對待而不會Mix-in。
從15行到31行,是真正的Mix-in的處理。處理的對象是__all__列表中所有的元素。
16行,根據屬性的字符串名,取出分布類模塊中相應的屬性。
17行,得到屬性相應的類型。
18行到27行,如果屬性類型為字典,元組(tuple),列表時,當類已經存在同名的值時,將新值添加到原值中;當不存在同名的值時,將新值加入到類中即可。
28行到31行,如果屬性為其它類型,則先刪除原值,再增加新值。
33行到38行,實現將“包名.模塊名”形式的模塊導入,返回模塊對象。此處不詳說了。
結論
分布類編程在軟件開發階段可以作為一種調試與功能編寫的有力方法。同時在軟件應用階段可以作為一種插件技術而使用。大家要注意的是,本文實例中所介紹的具體的分布類的構成與Mix-in方法的實現都是基於FlyEdit的實現。大家在實現自已的項目時應使用自已的方式。
--------------------------------------------------------------------------------
[1] 關於Mix-in技術的文章見本人寫的《Mix-in技術介紹》。
原文見我的主頁,可能有格式問題。
--------------------------------------------------------------------------------
[1] 關於Mix-in技術的文章見本人寫的《Mix-in技術介紹》。
原文見我的主頁,可能有格式問題。