最近由于老板需要做一個(gè)基于ArcGIS的地理分析工具。
經(jīng)過分析權(quán)衡,最終選用了Python作為開發(fā)語言,開發(fā)出的工具將在ArcToolbox中運(yùn)行。
由于需要存在比較復(fù)雜的用戶交互,ArcToolbox自帶的界面無法滿足需求,因此使用了PyQt做用戶界面。
這差不多還是我頭一次用腳本開發(fā)一個(gè)完整的應(yīng)用。麻雀雖小(就數(shù)千行代碼),但也五臟俱全。
此帖就來總結(jié)一下做這個(gè)工具的某些經(jīng)驗(yàn)教訓(xùn)。
一方面是自我總結(jié),一方面也希望給各位迄今為止沒用腳本干過大程序的靜態(tài)語言擁躉的筒子們以微不足道的啟發(fā)。
先來討論一下設(shè)計(jì)上的差異。
說到設(shè)計(jì),便會(huì)討論到設(shè)計(jì)模式。
對(duì)于設(shè)計(jì)模式的經(jīng)典圖書《Design Patterns》而言,它所提出的模式基本是針對(duì)靜態(tài)語言,有關(guān)設(shè)計(jì)模式的實(shí)現(xiàn)方案的討論,也多半是針對(duì)C++一類的強(qiáng)類型靜態(tài)語言。在Python的開發(fā)過程中,會(huì)發(fā)現(xiàn)很多模式(或者其它的慣用手法)在原有實(shí)現(xiàn)的基礎(chǔ)上有不小的變化,甚至一些原則也有所變動(dòng)。
最典型的例子是,在C++中,會(huì)提倡使用接口繼承而不是實(shí)現(xiàn)繼承;實(shí)現(xiàn)繼承改由委托來完成等一系列原則。
原先在C++中用接口繼承來分割接口與實(shí)現(xiàn),在Python中,可以完全不使用繼承,而采用動(dòng)態(tài)類型的特性,以類似于運(yùn)行期Concept的方式達(dá)到接口的目的;
相對(duì)的,繼承盡管也用于設(shè)計(jì)中,但是更主要的是以extend這樣的方式對(duì)原有的類進(jìn)行功能擴(kuò)展,使得行為類似于decorator模式;減少擴(kuò)展所需要的代碼量。
同時(shí),python不支持重載(因?yàn)槎际莿?dòng)態(tài)類型,也確實(shí)沒法重載),如果想重載只能在函數(shù)體中用instance of這樣的函數(shù)進(jìn)行判斷并手工分派(你拿字典分派也是手工分派),至少我目前只知道這個(gè)辦法。為了避免不必要的錯(cuò)誤,建議大家還是用命名對(duì)多個(gè)重載函數(shù)顯示的區(qū)分,對(duì)于需要重載的構(gòu)造函數(shù),還是用Factory Method比較好。
關(guān)于重構(gòu),如果腳本沒有對(duì)應(yīng)的測(cè)試,一定不要重構(gòu)。對(duì)于靜態(tài)語言,這一條件要稍顯寬松,但是在Python這樣的動(dòng)態(tài)語言上,就連Rename這樣的小型重構(gòu),都需要測(cè)試的保證,因?yàn)閹缀跛械腻e(cuò)誤只有在運(yùn)行期才能被檢定出來。
其次,說一下代碼編寫的問題。
和C++相比,Python是很節(jié)省代碼的。一方面要?dú)w功于語言機(jī)制,另一方面,Python豐富多樣的標(biāo)準(zhǔn)庫也為我們節(jié)省了不少的代碼量。
先來說說語言機(jī)制。
python一個(gè)很好的地方,就在于它將list、tuple、set、map/dict作為了build-in的要素,python的語法為這些要素提供了first class的支持。這使得我們?cè)诰帉懭萜飨嚓P(guān)操作時(shí)可以非常的方便。通常程序中大量存在類似的操作,在靜態(tài)語言中大量的語句可以在python中一句概括;在實(shí)際的編碼過程中,一定要靈活運(yùn)用Python容器操作,寫出干凈利落的代碼。
其二就是動(dòng)態(tài)類型也讓我們不需要為類型約束填寫過多的代碼,比如不必要的繼承與接口定義。這些代碼的節(jié)省其實(shí)是很可觀的。
其三,lambda、可調(diào)用體(其實(shí)就是仿函數(shù))被語言機(jī)制直接支持,也是能節(jié)省大量代碼的重要因素。憑借仿函數(shù),可以寫出大量在C++中難以編寫出的簡(jiǎn)潔優(yōu)雅的代碼。雖然boost費(fèi)勁心思提供了functor與lambda,但是這些庫編譯之慢,調(diào)試之辛苦,相比大家都是有感觸的。
個(gè)人體會(huì):
首先,靈活運(yùn)用語法優(yōu)勢(shì),特別是一些通用的初始化格式,以及一些特殊的寫法,比如list的構(gòu)造格式,slice等。這點(diǎn)往往也是腳本和靜態(tài)語言相比最大的優(yōu)勢(shì)。
其次靈活運(yùn)用內(nèi)建函數(shù)。內(nèi)建函數(shù)往往最能發(fā)揮語法優(yōu)勢(shì),甚至可以填補(bǔ)一些語法上的空缺。個(gè)人印象最深的,要數(shù)map/reduce/zip/lambda這幾個(gè)函數(shù)/語言機(jī)制。這些東西運(yùn)用好了,能很大程度上簡(jiǎn)化本來復(fù)雜的循環(huán)代碼。當(dāng)然,對(duì)于多層循環(huán)而言,個(gè)人不太建議用嵌套的map一類的函數(shù),外層的還是展開寫可讀性比較強(qiáng),內(nèi)層則保留以簡(jiǎn)化結(jié)構(gòu);
同時(shí)我也不太愿意用大量的lambda函數(shù),因?yàn)閘ambda函數(shù)本身很占版面,用多了代碼不那么好讀。必要的時(shí)候,還是用def定義出去比較好了。可調(diào)用體用恰當(dāng)了,能簡(jiǎn)化代碼,但是用的太多或者用法不好,也會(huì)影響可讀性。
同時(shí),python對(duì)于內(nèi)建類型的模擬做的很好,它提供了一系列buildin function的重寫方法,可以達(dá)到完全亂真的目的,這一點(diǎn)做的比C++還要好。
常規(guī)的內(nèi)建函數(shù)就不討論了,Python的Ref上都有。
討論一下__getattr__, __setattr__這兩個(gè)函數(shù)。如果我們用getattr函數(shù),或者XXX.xxx這樣的方法取得對(duì)象的一個(gè)成員,python首先會(huì)到對(duì)象內(nèi)建的屬性字典中查找。如果找不到,要么raise一個(gè)exception出來,如果重寫了__getattr__,那就調(diào)用這個(gè)函數(shù)。因此這兩個(gè)函數(shù)實(shí)際上是實(shí)現(xiàn)了屬性get/set的掛鉤。一般來說,我用get/set都不是為了單純的get/set,實(shí)際上是為了保持對(duì)象內(nèi)部的一致性,訪問的安全性,細(xì)節(jié)的封裝性等等目的,不同的屬性不是那么容易就用同樣的邏輯代碼。
本來像C#那樣為每個(gè)屬性提供獨(dú)立的get/set其實(shí)挺好的。Python則把所有的成員的get/set都攏到一起了。如果存取的附加代碼稍有差異,就容易寫出if...elif...elif...else這樣的分支代碼。
對(duì)這個(gè)問題,我是這樣做,屬性對(duì)應(yīng)的成員變量放入字典中;每個(gè)需要做復(fù)雜存取操作的屬性,有一個(gè)存取函數(shù),存取操作的區(qū)別用if分開,屬性與存取函數(shù),則用一個(gè)字典來關(guān)聯(lián)。這樣,attr函數(shù)首先訪問存取函數(shù)的字典,按照需要執(zhí)行存取操作。如果屬性不對(duì)應(yīng)存取函數(shù),那么就直接訪問屬性字典。
對(duì)于查找不到的情況,建議先捕獲異常,然后仿照的內(nèi)建的屬性訪問異常拋出。
當(dāng)然,有時(shí)候我也完全不用屬性,直接使用get/set函數(shù)的形式。不過這樣的話還是不如屬性來的方便。況且有時(shí)候括號(hào)漏寫了,代碼直接就來個(gè)運(yùn)行期異常,也是挺郁悶的。
最后討論下私有函數(shù)。對(duì)于python來說不存在真正的私有函數(shù),一般來講,要表達(dá)“私有實(shí)現(xiàn)”都是用“_”開頭的命名約定。
最后,討論一下調(diào)試和測(cè)試。
恐怕保證程序的正確性上,腳本還是要比靜態(tài)語言難。缺乏靜態(tài)檢測(cè)的腳本,連拼寫錯(cuò)誤都要延期到運(yùn)行期才能被檢測(cè)出來。因此大量腳本的調(diào)試,還是相當(dāng)痛苦的。
在這里,確保有有效的單元測(cè)試對(duì)腳本比對(duì)靜態(tài)語言要重要許多。這次做的工具,一開始并沒在意,但是到了中期以后,發(fā)現(xiàn)調(diào)試占用了大量的時(shí)間(因?yàn)锳rcGIS本身啟動(dòng)速度就比較慢,執(zhí)行一個(gè)調(diào)試周期很長),才開始給代碼部分地方補(bǔ)充了一些單元測(cè)試。有了單元測(cè)試以后,很大程度上縮短了調(diào)試周期。
單元測(cè)試也是腳本重構(gòu)的必要條件。
對(duì)于腳本而言,由于代碼量比傳統(tǒng)語言少很多,因此利用TDD一類測(cè)試先行的方法恐怕比在靜態(tài)語言上的收益要大得多。
在腳本中,偶爾要寫一些防衛(wèi)代碼。在工程的早期,我在防衛(wèi)代碼,特別是類型約束的代碼上做了大量的工作。但是后來發(fā)現(xiàn),這些防衛(wèi)代碼本身引入的錯(cuò)誤不必原始工程來的少。因此中期之后,對(duì)于內(nèi)部的類,撤消了大部分的類型防衛(wèi)代碼,而改用測(cè)試保證內(nèi)部邏輯的一致性。這樣減輕了代碼量,提高了可讀性。
--------------------------------
其實(shí)腳本對(duì)于Web開發(fā)來說一點(diǎn)都不陌生。上次跟老李說這事,他說他至少寫了萬行的jsp和vbs代碼,調(diào)試不難,但是要保證正確性很難。測(cè)試很大程度上成為了腳本的救命稻草。
一般來說,腳本比靜態(tài)語言省代碼,比靜態(tài)語言方便,比靜態(tài)語言XX,但是如果沒有測(cè)試,這些,都是鏡花水月而已。