1 引 言
語言是交流的工具,計算機語言是人與機器交流的工具。從廣義上講,任何描述算法和數(shù)據(jù)結(jié)構(gòu)的符號都可以構(gòu)成一種程序語言,但計算機語言是指那些在計算機上實現(xiàn)、能被計算機所理解的語言。自從20世紀(jì)50年代最早的高級語言出現(xiàn)以來,隨著計算機硬件價格廉價化和工作高速化,新思想和新技術(shù)不斷涌現(xiàn),特別是Internet的出現(xiàn),對計算機語言的發(fā)展產(chǎn)生了深遠(yuǎn)的影響。從早期二進制機器代碼,到增加了助記符的匯編語言,再到面向特定應(yīng)用領(lǐng)域的語言,以及現(xiàn)在功能越來越強大的系統(tǒng)語言。
縱觀程序設(shè)計語言的發(fā)展,一個很顯著的特點的是以機器的性能換取人的效率,以提高開發(fā)者的工作效率和滿足人的需求為目標(biāo),在所有的資源中人是最昂貴的資源,因而程序的編寫從面向機器逐漸向面向人過渡。早期二進制代碼是完全面向機器的,機器可以直接執(zhí)行,不需要任何編譯或解釋環(huán)節(jié),因而執(zhí)行效率極高,但用二進制編寫程序?qū)τ谌藖碚f,不但枯燥無味而且工作效率極其低下,一連串的雜亂無章01代碼要人去理解和記憶是不可能的。為了提高人的工作效率和程序的可讀性,于是對二進制代碼表示的機器指令換成便于人們識別記憶的符號,便形成了帶有助記符的匯編語言,人們用匯編語言編寫匯編指令操縱機器,具體操作可直接作用到底層硬件的某個存儲單元、寄存器和設(shè)備接口,程序的執(zhí)行速度并沒有受到很大的影響,較之機器代碼,人的工作效率顯然得到了一定的改善,人們只需要識記相對簡單的匯編指令集,但是匯編指令仍然是面向機器的,不同機器上有不同的指令集,機器的體系結(jié)構(gòu)對匯編指令源程序的影響很大,在一臺機器上能夠運行的程序往往在另一臺機器上不能正常運行,開發(fā)大型程序的工作量極大,后期的維護工作也很困難。
20世紀(jì)60年代中期,隨著半導(dǎo)體技術(shù)的發(fā)展和成熟,機器硬件代價不斷下降,而編程的代價在上升,同時也出現(xiàn)各種各樣的軟件需求,不同應(yīng)用領(lǐng)域提出了不同的應(yīng)用需求,開發(fā)的軟件不但要便于移植還要便于后期的維護,現(xiàn)有的軟件技術(shù)是無法滿足人們對軟件提出的要求,為了能夠解決這些問題,迫切需要產(chǎn)生新的程序設(shè)計語言,從根本上改變?nèi)藗兙帉懗绦虻姆绞健_@個時期,編譯器技術(shù)進一步走向成熟,高級語言應(yīng)運而生。高級語言類似自然語言又區(qū)別與自然語言,對人來說很容易理解和學(xué)習(xí),在語法、句法和文法方面都有嚴(yán)格的要求,比起匯編語言,高級語言不僅容易記憶、掌握和使用,而且是面向問題的,為特定應(yīng)用領(lǐng)域的某些問題提供了良好的解決方案。二進制代碼是機器可以執(zhí)行的代碼,因此機器要理解并執(zhí)行一種高級語言就必須要有一個“中介”充當(dāng)語言與機器的橋梁作用,這個“中介”根據(jù)語言的性質(zhì)分為編譯器和解釋器,其用途就是將高級語言翻譯為機器可以理解的語言。
軟件技術(shù)的發(fā)展史就是程序設(shè)計語言的發(fā)展史。高級語言的產(chǎn)生促進了軟件技術(shù)的革新和發(fā)展。自從有了高級語言,軟件界發(fā)生了日新月異的變化,各種新的軟件技術(shù)層出不窮。面向過程語言的產(chǎn)生使得結(jié)構(gòu)化程序設(shè)計成為軟件開發(fā)最基本的方法,程序的流程控制是分析程序必不可少要素之一,許多技術(shù)創(chuàng)新為后來的軟件開發(fā)技術(shù)發(fā)展鋪墊了一條寬闊的道路,如模塊化設(shè)計思想、結(jié)構(gòu)化設(shè)計、信息隱藏,并成為軟件工程的早期發(fā)展,為面向?qū)ο笳Z言的產(chǎn)生提供了寶貴的技術(shù)來源。當(dāng)然,從軟件工程的角度看,面向過程語言難以開發(fā)出可復(fù)用性、可擴展性、易于維護的軟件,面向?qū)ο笳Z言克服了面向過程語言的缺點,將面向?qū)ο蠹夹g(shù)都融合到語言中,在語言一級支持不同層次的模塊化設(shè)計,更好的實現(xiàn)了數(shù)據(jù)抽象和信息隱藏,繼承實現(xiàn)了軟件復(fù)用,多態(tài)便于動態(tài)重構(gòu),基本可以應(yīng)付一些簡單的需求變更,為開發(fā)出高質(zhì)量的軟件提供了一把利器,所謂“工欲善其事,必先利其器”,有了鋒銳的工具,做起事來當(dāng)然會又快又好。然而,“金無足金,人無完人”,面向?qū)ο笳Z言中的對象技術(shù)也存在與生俱來的缺陷,因此提出了面向方面的編程語言,將軟件關(guān)注點模塊化,彌補了面向?qū)ο缶幊痰牟蛔悖岣吡顺绦蚰K的內(nèi)聚度,更利于軟件的維護和復(fù)用。正是這些新技術(shù)將開發(fā)者從與機器打交道的繁雜中不斷解放出來,使開發(fā)者在有更多的時間和精力集中、專注分析和解決應(yīng)用領(lǐng)域某些問題的同時,又可以開發(fā)出高質(zhì)量的軟件。
每一次軟件技術(shù)的飛躍發(fā)展都與新型程序設(shè)計語言的出現(xiàn)分不開,新的軟件技術(shù)構(gòu)建了一個新的環(huán)境,新的環(huán)境對軟件技術(shù)又提出了更高的要求。早期受到計算機硬件性能和價格的限制,人們主要關(guān)注點是如何充分利用硬件提高軟件的性能,如今,硬件技術(shù)在“摩爾定律”的支配下飛躍發(fā)展,計算機運行速度和性能大大提高,人們對軟件的要求發(fā)生了根本的轉(zhuǎn)變。隨著計算機技術(shù)深入各個應(yīng)用領(lǐng)域,軟件應(yīng)用的領(lǐng)域不斷擴大,特定應(yīng)用領(lǐng)域涌現(xiàn)出來的、需要借助計算機才能解決的問題千奇百怪,面對的問題也越來越復(fù)雜,對軟件的需求也越來越高,需要有更多、更好、更新的技術(shù)來解決遇到的問題。一個高質(zhì)量的軟件要求能夠適應(yīng)需求變更,不能適應(yīng)需求變更的軟件所付出的返工代價是慘痛的?,F(xiàn)在越來越多的應(yīng)用甚至要求系統(tǒng)能夠在運行中修正和改變,所做出的改變都是開發(fā)人員和用戶難以預(yù)測的,需要軟件本身具備這種處理能力,能夠支持運行時的動態(tài)重構(gòu)。以C++、Java、C#為代表的典型的高級語言雖然在一定程度上支持運行時動態(tài)性,比如虛函數(shù)實現(xiàn)的多態(tài)性,可以在運行時確定調(diào)用的成員函數(shù)是父類還是子類的,以及RTTI具有的在運行時識別、加載和管理類的能力,但是在運行時并不能改變類或變量的類型,引用變量前必須聲明變量的類型,它們都屬于靜態(tài)語言,在解決某些問題受到了極大的限制,很多技術(shù)難以實現(xiàn),尤其是實現(xiàn)運行時動態(tài)重構(gòu)。
近幾年來,以Python、Perl、Tcl、Ruby為代表的動態(tài)語言(又稱腳本語言)越來越受到開發(fā)者的青睞,使用動態(tài)語言群體、社區(qū)不斷壯大,許多開發(fā)團隊不再單純使用一種語言編程開發(fā)軟件,而是混合式編程,動態(tài)語言作為膠水語言專為應(yīng)用程序而設(shè)計,在今后的混合式應(yīng)用中變的越來越重要,甚至有人認(rèn)為是“21世紀(jì)更高級的編程語言”[1]。為什么動態(tài)語言一族在新型語言層出不窮的今天可以異軍突起,受到如此青睞和厚愛?動態(tài)語言是一些什么樣的語言?具有什么優(yōu)勢?究竟能夠解決哪些靜態(tài)語言所不能解決的問題?動態(tài)語言和靜態(tài)語言二者有哪些區(qū)別?程序設(shè)計語言的未來發(fā)展又將是如何?本文接下來主要討論的就是這些問題,首先從程序設(shè)計語言的基本原理和概念入手,采用與靜態(tài)語言對比的方式逐步深入介紹動態(tài)語言的語言特征,最后給出了動態(tài)語言實例Python作為研究對象,簡要的介紹Python的語言特性,并嘗試用Python實現(xiàn)了GoF的幾種設(shè)計模式。
2 基本概念
2.1虛擬機、編譯器和解釋器
實現(xiàn)一種計算機程序設(shè)計語言時,運行時的數(shù)據(jù)結(jié)構(gòu)和程序執(zhí)行中的算法定義了一臺虛擬機。該虛擬機的機器語言是由語言翻譯器所產(chǎn)生的可執(zhí)行代碼,如果語言是編譯型的,則它的形式就是實際的計算機二進制代碼,實際的機器可以直接執(zhí)行;如果語言是解釋型的,則結(jié)構(gòu)和形式可以是任意的。該虛擬機的數(shù)據(jù)結(jié)構(gòu)就是程序執(zhí)行時的數(shù)據(jù)結(jié)構(gòu),基本操作是那些在執(zhí)行時實際執(zhí)行的操作。編譯型語言C所定義的虛擬機的機器代碼是二進制代碼。C源程序要生成可執(zhí)行程序需要經(jīng)過C編譯器編譯成目標(biāo)機器代碼,不同的平臺要編譯成不同的目標(biāo)代碼。Java虛擬機是Java程序的運行環(huán)境,Java虛擬機是一個虛擬的計算機,它有自己的處理器、堆棧、寄存器,還有相應(yīng)的指令系統(tǒng),同時也有支持的數(shù)據(jù)結(jié)構(gòu)和基本操作。要運行任何Java程序首先必須通過Java編譯程序翻譯成Java虛擬機可以理解的目標(biāo)碼(Java虛擬機將這種機器碼稱為字節(jié)碼byte code),執(zhí)行Java程序時實際上是在Java虛擬機上執(zhí)行字節(jié)碼,虛擬機負(fù)責(zé)將字節(jié)碼解釋稱特定平臺上的機器指令,Java程序運行時的數(shù)據(jù)結(jié)構(gòu)就是虛擬機所定義的數(shù)據(jù)結(jié)構(gòu)。虛擬機在平臺和程序之間充當(dāng)了紐帶作用,承上啟下,進一步抽象了系統(tǒng)平臺,使得Java程序與具體機器的體系結(jié)構(gòu)、操作系統(tǒng)平臺無關(guān),因而成為Java語言的一個重要的特性。
在具體計算機上實現(xiàn)一種語言,首先要確定的是表示該語言語義解釋的虛擬計算機,一個關(guān)鍵的問題是程序執(zhí)行時的基本表示是實際計算機上的機器語言還是虛擬機的機器語言。這個問題決定了語言的實現(xiàn)。根據(jù)這個問題的回答,可以將程序設(shè)計語言劃分為兩大類:編譯型語言和解釋型語言。
由編譯型語言編寫的源程序需要經(jīng)過編譯、匯編和鏈接才能輸出目標(biāo)代碼,然后機器執(zhí)行目標(biāo)代碼,得出運行結(jié)果,目標(biāo)代碼由機器指令組成,一般不能獨立運行,因為源程序中可能使用了某些匯編程序不能解釋引用的庫函數(shù),而庫函數(shù)代碼又不在源程序中,此時還需要鏈接程序完成外部引用和目標(biāo)模塊調(diào)用的鏈接任務(wù),最后輸出可執(zhí)行代碼。C、C++、Fortran、Pascal、Ada都是編譯實現(xiàn)的。高級語言轉(zhuǎn)換為可執(zhí)行代碼的過程如下圖:
解釋型語言的實現(xiàn)中,翻譯器并不產(chǎn)生目標(biāo)機器代碼,而是產(chǎn)生易于執(zhí)行的中間代碼,這種中間代碼與機器代碼是不同的,中間代碼的解釋是由軟件支持的,不能直接使用硬件,軟件解釋器通常會導(dǎo)致執(zhí)行效率較低。用解釋型語言編寫的程序是由另一個可以理解中間代碼的解釋程序執(zhí)行的。與編譯程序不同的是,解釋程序的任務(wù)是逐一將源程序的語句解釋成可執(zhí)行的機器指令,不需要將源程序翻譯成目標(biāo)代碼后再執(zhí)行。對于解釋型Basic語言,需要一個專門的解釋器解釋執(zhí)行Basic程序,每條語言只有在執(zhí)行才被翻譯。這種解釋型語言每執(zhí)行一次就翻譯一次,因而效率低下。Java很特殊,Java程序也需要編譯,但是沒有直接編譯稱為機器語言,而是編譯稱為字節(jié)碼,然后在Java虛擬機上用解釋方式執(zhí)行字節(jié)碼。Python的也采用了類似Java的編譯模式,先將Python程序編譯成Python字節(jié)碼,然后由一個專門的Python字節(jié)碼解釋器負(fù)責(zé)解釋執(zhí)行字節(jié)碼。一般地,動態(tài)語言都是解釋型的,如Tcl、Perl、Ruby、VBScript、 JavaScript等。
2.2 綁定和綁定時間
綁定和綁定時間是程序設(shè)計語言中的十分重要的概念。在設(shè)計一門新的語言時,首先要確定的就是這種語言的綁定以及綁定發(fā)生的時間,為語言的特性和實現(xiàn)定下了一個基調(diào)。一段程序?qū)嶋H上是由不同元素組成的,這些元素包括實體和屬性,有時一個元素既可作為實體,也可作為屬性。在c語言中,語句int count = 5;中,int本身可以被認(rèn)為是實體—整數(shù)類型,它的屬性則是該類型所表示的整數(shù)范圍,16位機器上為−32768~32767;同時int也是變量count的屬性,表明count是一個整型變量。為了保證程序元素的語義完整性和可理解性,對于程序中的每個實體都要求有相關(guān)的語義信息,即要從一個可能的性質(zhì)集合中選擇一個性質(zhì)作為實體的性質(zhì)或?qū)傩灾担_定實體和屬性的約束關(guān)系。這種約束關(guān)系的確定稱為綁定,綁定相對于程序翻譯和處理的時間稱為綁定時間,綁定時間決定了語言的特性。
綁定可以發(fā)生從語言的設(shè)計和實現(xiàn)到程序的翻譯、連接和執(zhí)行的各個階段。在語言定義時,首先構(gòu)造了一個支撐語言的虛擬機,確定了一個語言的大部分結(jié)構(gòu),例如,選擇語句的形式、數(shù)據(jù)類型及其所能表示的范圍和程序結(jié)構(gòu)。在語言實現(xiàn)時,確定語言定義的細(xì)節(jié)在具體計算機上的實現(xiàn)方式,不同的實現(xiàn)存在著很大的差別。比如Microsoft C和Turbo C都是C語言的實現(xiàn),兩者實現(xiàn)的C語言大部分是相同的,但在一些方面存在著不同,因此會出現(xiàn)在Microsoft C可以順利運行的程序在Turbo C中不能運行。我們知道,要使一個以靜態(tài)文本方式存放的程序成為一個可執(zhí)行的程序要經(jīng)過編譯、連接和執(zhí)行步驟。在C/C++語言中,.c文件經(jīng)過編譯后,程序中變量的類型、數(shù)據(jù)成員的偏移量、數(shù)組的存儲方式以及靜態(tài)變量的存儲位置都確定下來,并且完成了類型檢查和轉(zhuǎn)換工作。對于文件作用域中尚未定義的外部符號,在連接步驟中連接程序負(fù)責(zé)在其他的編譯單元查找,并將找的符號名所表示的內(nèi)容綁定到目標(biāo)代碼中的符號生成可執(zhí)行代碼。因此,庫函數(shù)調(diào)用時函數(shù)名與函數(shù)體的綁定關(guān)系發(fā)生在連接過程中。一個已經(jīng)順利的通過編譯和連接的程序,一定可以順利地在計算機上運行嗎?答案是否定的。因為程序中還有一部分綁定需要在程序執(zhí)行時才能確定下來,如果在確定綁定關(guān)系時發(fā)生了錯誤,那么程序的運行也會發(fā)生異常。大部分語言中,這些綁定包括數(shù)據(jù)對象和其存儲單元(臨時存儲單元)、動態(tài)內(nèi)存分配、調(diào)用函數(shù)時的形參和實參的連接、形參和實際存儲單元的對應(yīng)關(guān)系、調(diào)用子程序的返回結(jié)果等等。
一般地,我們將綁定時間分為兩類:一類是在運行前確定綁定關(guān)系,稱之為“早綁定”;另一類是在運行時確定綁定關(guān)系,稱之為“晚綁定”。“早綁定”在程序運行前就把大部分程序元素的屬性確定下來,運行時只需要完成少部分的綁定,因而執(zhí)行效率高,但不夠靈活,要求程序員在程序運行之前盡可能確定程序元素的屬性,一旦確定下來修改起來就十分困難,需要重新編寫、編譯、連接整個程序。而“晚綁定”恰恰與之相反,它把程序中大部分綁定關(guān)系推遲到執(zhí)行時確定下來,程序執(zhí)行時要完成大部分的綁定工作,所以靈活但效率不高。“早綁定”和“晚綁定”的優(yōu)缺點實際上是語言的靈活性和效率的沖突。所謂“魚和熊掌不可兼得”,關(guān)鍵要根據(jù)語言設(shè)計的目標(biāo)在兩者之間尋找一個最佳的平衡點。優(yōu)先考慮執(zhí)行效率、以執(zhí)行效率為主要目標(biāo)的語言通常設(shè)計為綁定盡可能地在程序運行之前確定下來的“早綁定”,如Fortran、C、Pascal。而對于追求靈活性的語言來說,如Smalltalk、LISP、Python,ML,采用的則是“晚綁定”。有些語言(Ada)提供了對綁定時間的選擇機制,允許程序員自己根據(jù)實際情況選擇合適的綁定時間。
弄清了“早綁定”和“晚綁定”的概念及其優(yōu)缺點后,我們可以很容易地回答諸如“為什么Fortran語言在處理字符串方面的能力并不強”、“為什么C語言的執(zhí)行效率比Lisp語言要高”等問題。當(dāng)然,比較語言是為了更好的分析語言,不能盲目地評判不同語言之間孰好孰壞,每種語言都有自己的用武之地,都有自己的優(yōu)勢。
基于綁定時間的概念可以定義和比較語言的特性。在程序設(shè)計語言中,有兩個使用廣泛的、相對的術(shù)語——靜態(tài)和動態(tài)——實際上是根據(jù)綁定時間劃分的。一般提到“靜態(tài)”指的是在執(zhí)行前所發(fā)生的動作,“動態(tài)”是指在執(zhí)行時發(fā)生的動作。由此,“早綁定”也可稱為靜態(tài)綁定,“晚綁定”稱為動態(tài)綁定。一般來講,“靜態(tài)”具有執(zhí)行效率高而靈活性不夠的特點,相反,“動態(tài)”具有執(zhí)行效率低但靈活性高的特點,兩者是一對相對的術(shù)語,是一組對立統(tǒng)一的辨證關(guān)系。程序設(shè)計語言中有許多概念都涉及到這兩者,如靜態(tài)類型檢查和動態(tài)類型檢查、靜態(tài)內(nèi)存分配和動態(tài)內(nèi)存分配、靜態(tài)作用域和動態(tài)作用域,以及本文所闡述的靜態(tài)語言和動態(tài)語言。
3 動態(tài)語言
3.1 語言的動態(tài)特性
介紹動態(tài)語言之前,首先介紹一下語言的動態(tài)特性,并引出動態(tài)語言的定義。
語言的動態(tài)特性表示語言具有在運行時確定綁定關(guān)系的性質(zhì)。實際上,“靜態(tài)”是一個相對的概念,任何程序設(shè)計語言都可以看作具有某種程度的動態(tài)特性。一個變量能夠在運行時改變自身的值,可以說它具有動態(tài)特性,比如最簡單的賦值語句:
char ch;
int m;
ch = cin.get();//從輸入流中接收一個字符
m = func(); //將func的返回結(jié)果賦給m
對字符型變量ch與值的綁定關(guān)系要推遲到程序運行時檢測到標(biāo)準(zhǔn)I/O流的輸入數(shù)據(jù)才能確定下來,整型變量m的值也要到程序運行時執(zhí)行了函數(shù)func后才能確定。在運行時進行的動態(tài)類型檢查、動態(tài)內(nèi)存分配都是語言的動態(tài)特性的具體表現(xiàn)。變量的尋址方式也是一個動態(tài)的過程。程序經(jīng)編譯后,獲得了變量的相對于程序代碼段(CS)或數(shù)據(jù)段(DS)的偏移量信息,在程序執(zhí)行時,操作系統(tǒng)首先將程序裝載到內(nèi)存中足夠大小的某個單元中,這個單元的首地址加上變量的偏移量就形成了變量的存儲單元地址。此外,面向?qū)ο笳Z言中運行時多態(tài)性是一個重要的動態(tài)特性。從這個意義上說,早期Fortran語言、C、Pascal都具備一定程度的動態(tài)特性,但比較弱,而Smalltalk、Lisp、Python等語言在運行時可以改變變量的類型甚至?xí)r自身的程序結(jié)構(gòu),我們就認(rèn)為這樣的語言具有更強的動態(tài)特性。
不同的語言具有不同程度的動態(tài)特性??v觀程序設(shè)計語言的發(fā)展史,如果將Fortran和Lisp語言看作是語言在動態(tài)特性上的兩個極端的話,那么現(xiàn)代大多數(shù)語言都是介于二者之間的折中,綜合考慮了開發(fā)效率和執(zhí)行效率以及其他眾多因素。各種語言也在相互借鑒,不斷演變。下圖是以Fortran和Lisp為兩頭,比較了幾種語言的動態(tài)性程度:
在上圖中,從左到右語言的動態(tài)特性逐漸加強。處于最左端的Fortran語言不支持堆棧,所有的變量和子程序都是在編譯時分配好內(nèi)存的,不能進行動態(tài)內(nèi)存分配,因而不能進行函數(shù)遞歸調(diào)用,許多問題的解決方式受到極大的限制。這主要是由早期Fortran語言的設(shè)計目標(biāo)決定的,早期Fortran語言主要是為了解決科學(xué)和工程中的計算問題,優(yōu)先考慮的是語言的執(zhí)行效率。雖然Fortran語言被無數(shù)次認(rèn)定為過時,然而它仍然繼續(xù)發(fā)展著。Fortran90大大擴展了傳統(tǒng)Fortran的功能,增加了現(xiàn)代高級語言的數(shù)據(jù)和控制特征,允許內(nèi)存的動態(tài)分配,使得它具有和C和Pascal語言相當(dāng)?shù)哪芰Α?br>比Fortran動態(tài)性更強的C/C++語言提供了指針,支持堆棧,提供了malloc/new和free/delete操作,運行時可以動態(tài)分配和釋放內(nèi)存,可以比較靈活地動態(tài)生成對象并分配存儲空間。此外,C++語言中的RTTI(RunTime Type Identification)機制可以在只有指向某個對象的基類指針的情況下,根據(jù)駐留在虛函數(shù)表中的類型信息,在運行時確定該對象的準(zhǔn)確類型。然而,C/C++程序中的變量類型仍然需要在編譯時確定下來,大部分類型檢查也是在編譯時完成的,執(zhí)行前完成了大部分的綁定工作。
現(xiàn)在流行的Java和C#語言的動態(tài)特性之所以要比C/C++強,是因為Java/C#提供了更強的反射Reflection機制,可以在運行時通過Reflection APIs取得任何一個已知名稱的class的內(nèi)部信息,包括其modifiers(諸如public, static 等等)、superclass(例如Object)、實現(xiàn)的interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于運行時改變fields內(nèi)容或喚醒methods。如在C#中,可以通過System.Type類在運行時獲取任何給定數(shù)據(jù)的類型信息,包括構(gòu)造函數(shù)、屬性、方法和事件,也可以喚醒(Invoke)相關(guān)的方法(具體的使用可參考Microsoft出版的《Inside C#》的“Querying Metadata with Reflection”一章)。
而Smalltalk以右的語言,變量的類型綁定和類型檢查都是在運行時進行的。Perl、Python和Ruby語言可以在運行時修改類的結(jié)構(gòu)或定義,變量的類型可以按需改變,編寫程序時無須聲明變量類型。Lisp語言甚至可以在運行時動態(tài)地改變自身的代碼, Lisp語言對程序代碼和數(shù)據(jù)一視同仁,都看作是存儲在內(nèi)存中的對象,這樣數(shù)據(jù)結(jié)構(gòu)既可作為程序執(zhí)行,程序也可作為數(shù)據(jù)進行修改。
上面介紹的幾種語言都具備一定程度的動態(tài)特性,那么應(yīng)該如何定義動態(tài)語言呢?目前在許多開發(fā)社群中,提及“動態(tài)語言”,一種普遍的觀點是認(rèn)為“動態(tài)語言是指能夠在運行時改變程序結(jié)構(gòu)和變量類型的語言”,有時也稱作“動態(tài)類型語言”(Dynamic Type Language),本文采用的就是這種觀點,在下文中的動態(tài)語言都是指具有這種性質(zhì)的語言,與之相對的概念“靜態(tài)語言”指的是在編譯階段確定變量或數(shù)據(jù)對象類型的語言。按照這種說法,上圖中Smalltalk/Perl以左的為靜態(tài)語言,以右的語言都為動態(tài)語言。這里需要說明的是,許多技術(shù)文章和語言研究中都涉及到“腳本語言”的概念,實際上腳本語言就是動態(tài)語言,因為腳本語言也具備在運行時動態(tài)改變類型的性質(zhì),符合動態(tài)語言的定義,本文不區(qū)分這兩個概念,一律使用“動態(tài)語言”,強調(diào)該類語言的最明顯的特征。另外,有的人也將“靜態(tài)語言”稱之為“系統(tǒng)程序設(shè)計語言”,因為大多數(shù)靜態(tài)語言都是系統(tǒng)程序設(shè)計語言,這兩個概念只是從不同的角度描述了同一類語言,各自的側(cè)重點不同,“靜態(tài)語言”的“靜態(tài)”是基于綁定時間的,強調(diào)了語言中大部分綁定關(guān)系是發(fā)生在運行前,而“系統(tǒng)程序設(shè)計語言”強調(diào)語言所能完成的功能是側(cè)重于編寫系統(tǒng)程序。
3.2 動態(tài)語言的特點
近幾十年來,傳統(tǒng)的軟件開發(fā)模式和工具、編程語言已難以適應(yīng)軟件需求的變化,軟件的混合式開發(fā)逐漸增加。隨著動態(tài)語言的興起,人們的編程方式和觀念發(fā)生了轉(zhuǎn)變。在諸如C/C++、Java、C#之類的靜態(tài)語言中,語法規(guī)則迫使程序員在使用一個變量前首先要聲明它的類型,變量的類型一旦確定下來,那么在整個程序中都不能改變,只能是一種類型。而在象Smalltalk、Python、Perl、Tcl、Ruby、Visual Basic和Unix Shells的動態(tài)語言中,無類型化定義使程序員從龐大的類型系統(tǒng)解脫出來,變量不經(jīng)聲明就可以直接使用,變量的類型可以按需改變,編寫程序變得輕松自由。本節(jié)主要介紹動態(tài)語言的特點和編程方式,以及與靜態(tài)語言的主要區(qū)別和聯(lián)系。
動態(tài)語言,顧名思義,最顯著的特點就是在于它的“動態(tài)性”,即運行時可以按需改變程序結(jié)構(gòu)和變量類型、函數(shù)的定義等,這也是與靜態(tài)語言的根本區(qū)別。除此之外,動態(tài)語言還有許多特點,這些特點深受開發(fā)人員的喜愛,使得動態(tài)語言漸入人心。
動態(tài)類型
類型系統(tǒng)影響了語言的諸多特性。在靜態(tài)語言中,聲明的目的是為了告訴編譯器所使用的變量“是什么”,使類型檢查可以在編譯階段靜態(tài)地進行,盡量減少執(zhí)行時的類型檢查,提高執(zhí)行效率,但缺乏靈活性。而在動態(tài)語言中,沒有聲明語句,賦值語句將名稱綁定在數(shù)據(jù)對象上,如果名稱賦予數(shù)據(jù)對象一種類型,稍后也可以給對象賦予另外一種類型。變量被設(shè)計成無類型的,變量的類型可以按需改變,同一個變量既可作為整型的,也可作為字符串,還可用來定義函數(shù)。
我們知道,程序中定義的操作一般需要特定類型的參數(shù)作為操作的輸入,操作只有在接收到類型正確的參數(shù)時才能正確無誤的執(zhí)行,最典型的實例就是函數(shù)的定義,函數(shù)的原型包括函數(shù)的參數(shù)列表和返回值類型,參數(shù)列表提供輸入?yún)?shù)的全部信息,執(zhí)行函數(shù)前首先要進行參數(shù)類型的檢查。在動態(tài)語言中,變量是無類型的,那么如何保證所執(zhí)行的操作是否接收到類型正確的參數(shù)呢?在運行時進行動態(tài)類型檢查機制解決了類型安全這一問題。動態(tài)類型檢查通過在每個數(shù)據(jù)對象中保存一個類型標(biāo)簽表明該數(shù)據(jù)對象的類型,比如在表達(dá)式C=A+B中,A和B的類型在程序運行時確定,也可以在運行時改變,所以每次執(zhí)行 + 操作時都要根據(jù)類型標(biāo)簽對A和B的類型進行檢查,只有在類型正確的情況下才能執(zhí)行,否則發(fā)出錯誤信號。操作正確執(zhí)行后,確定了變量C的類型,并記下C的類型標(biāo)簽以備隨后可能的操作進行類型檢查。
顯然,動態(tài)類型檢查不能靜態(tài)地檢測到程序代碼中類型不匹配的錯誤,并不意味著動態(tài)類型容易在程序中引入類型安全的錯誤。事實上,在諸如C++、Java之類的靜態(tài)類型語言中雖然能夠在編譯時盡可能多的檢測到程序中類型失配的錯誤,但類型僅僅是數(shù)據(jù)的一小部分信息,類型正確并不能保證程序中不存在其他的錯誤。在大規(guī)模的程序中,要為類型上編寫大量的語句,這就使得程序員專注于程序中的類型正確性而容易忽視程序其他部分的正確性。有些問題用靜態(tài)類型很難實現(xiàn),例如對于一個不支持泛型編程的語言來說實現(xiàn)一個可以支持多種類型的容器類就比較困難,假如編寫了一個可以存放objects的容器類,在具體應(yīng)用時要通過向上或向上轉(zhuǎn)換成所需的類型,而這種轉(zhuǎn)換往往是不安全的。而動態(tài)類型語言不需要這種轉(zhuǎn)換,它所實現(xiàn)的容器類完全是泛型的,在語言一級就提供了良好的支持[3]。裘宗燕教授則認(rèn)為“很多人之所以在最初使用具有豐富動態(tài)特征的語言編程時容易犯錯誤,主要是因為他們習(xí)慣了諸如C、Pascal等語言,不了解用這些動態(tài)特征豐富的語言中編程的一些必要的風(fēng)格和習(xí)慣。如果熟悉了這些風(fēng)格和習(xí)慣,犯錯誤這件事情同樣是可能很好地避免的。使用弱類型的語言同樣可以開發(fā)很好的系統(tǒng),而且實際上已經(jīng)開發(fā)了很多很好的系統(tǒng)。”[6]。
動態(tài)類型檢查的主要優(yōu)點在于程序設(shè)計的靈活性,不需要聲明語句,一個變量名綁定的數(shù)據(jù)對象的類型可以在程序執(zhí)行時按需改變,使程序員從數(shù)據(jù)類型擺脫出來,同時也可以編寫更少的程序代碼行完成同樣的功能。動態(tài)類型是動態(tài)語言的最顯著的優(yōu)點,但也是動態(tài)語言的弱點根源所在。運行時進行的類型檢查也存在幾點重大不足:
ü 程序難以調(diào)試。因為動態(tài)類型檢查只在程序運行到某一條操作時才對其進行類型檢查,而ü 從來不ü 檢查沒有被執(zhí)行的執(zhí)行路徑上的操作。在軟件測試時是不ü 能遍歷程序中所有的執(zhí)行路徑,ü 這樣沒有被執(zhí)行的路徑仍有可能存在bugs。這一點可能是動態(tài)語言致命的缺點,ü 它導(dǎo)致了動態(tài)語言對開發(fā)大型軟件項目支持力度不ü 夠。
ü 保存大量的類型信息。運行時需要相當(dāng)大的額外存儲空間。
ü 執(zhí)行效率低。動態(tài)類型檢查要靠軟件模擬實現(xiàn),ü 主要是在運行時完成的,ü 所以在執(zhí)行速度上降低了不ü 少。
靜態(tài)類型和動態(tài)類型都有各自的優(yōu)點和缺點,不能簡單地認(rèn)為靜態(tài)類型比動態(tài)類型要好或者動態(tài)類型比靜態(tài)類型要好[3],關(guān)鍵要看具體應(yīng)用的場合。
開發(fā)效率高
在動態(tài)語言社區(qū)里,值得開發(fā)人員津津樂道的是動態(tài)語言具有比靜態(tài)語言高數(shù)倍的開發(fā)效率。雖然靜態(tài)語言在執(zhí)行效率上比動態(tài)語言略勝一籌,但在開發(fā)效率上只能甘拜下風(fēng)。正如前文所提到,程序設(shè)計語言逐步從面向機器到面向人過渡,以犧牲運行效率來換取開發(fā)效率。如今,計算機硬件性能的大大提升,我們能夠承受起動態(tài)語言在執(zhí)行效率上的損失,實際上,只有很少的應(yīng)用才需要盡可能的利用機器的性能。目前,普遍認(rèn)為Python的開發(fā)效率要比Java高出5-10倍。那么動態(tài)語言為什么會在開發(fā)效率方面具有天獨厚的優(yōu)勢?下面就從幾個方面進行說明:
1)易學(xué)易用,編程人員可快速上手
Python、Perl、Ruby等動態(tài)語言具有簡潔的語法規(guī)則,交互式的編程環(huán)境,比C++、Java等靜態(tài)語言更容易學(xué)習(xí)和掌握。比如Python語言,語言的語法結(jié)構(gòu)、控制結(jié)構(gòu)和C/C++十分類似,對于一個熟悉C、C++語言的編程人員來說,花3-5個小時完全可以掌握Python的語法特性。軟件的復(fù)雜性在于整合了千絲萬縷的關(guān)系,交互式環(huán)境把程序分解成一塊塊,對于不熟悉的語言特性,可以方便輸入交互式命令進行測試。動態(tài)語言不需要聲明語句的編程方式使得動態(tài)語言更加接近于自然語言,簡單明了,易學(xué)易讀,許多優(yōu)秀的語言特性都是語言大師深思熟慮之后的產(chǎn)物。對于一個軟件項目來說,項目經(jīng)理更愿意選擇一門易于上手又同樣可以完成任務(wù)的編程語言作為軟件的開發(fā)語言,一則可以減少員工的學(xué)習(xí)成本,二則有利于縮短項目完工的時間。
動態(tài)語言的易學(xué)易用性最終導(dǎo)致了編程群體的改變。二十年前大多數(shù)編程者是大型項目的專業(yè)編程人員.那個時代的編程人員需要花幾個月的時間掌握一門語言和它的編程環(huán)境,系統(tǒng)程序設(shè)計語言(靜態(tài)語言)就是為這些人設(shè)計的.然而,自從個人電腦出現(xiàn)以后,越來越多的非專業(yè)編程者加入到編程者的行列.對這些人來說,編程不是他們的主要工作,而只是他們偶爾用來幫助他們工作的工具。偶爾編程的例子是編寫簡單的數(shù)據(jù)庫查詢或者是電子數(shù)據(jù)表的宏.非專業(yè)編程者不希望花幾個月的時間學(xué)習(xí)一門專為系統(tǒng)程序而設(shè)計的靜態(tài)語言,但他們可以花幾個小時的時間學(xué)到足夠的動態(tài)語言知識來寫出有用的腳本代碼。由于動態(tài)語言有簡單的句法并且省略了諸如對象和線程的復(fù)雜特性,因而它比靜態(tài)語言要更容易學(xué)習(xí)和掌握。例如,非專業(yè)編程人員很少會選擇Visual C++,而大部分會用Visual Basic編寫有用的應(yīng)用程序[1]。
2)內(nèi)置豐富的數(shù)據(jù)結(jié)構(gòu)和操作
實際上,動態(tài)語言是建立系統(tǒng)程序設(shè)計語言之上的語言,大多數(shù)動態(tài)語言解釋器的內(nèi)核都是用某種靜態(tài)語言實現(xiàn)的,Python是用C實現(xiàn)的,JVM上的Jython是用純Java實現(xiàn)的,.Net CLR上的IronPython是用C# 實現(xiàn)的。動態(tài)語言將靈活性作為其設(shè)計的目標(biāo),優(yōu)先考慮了語言的靈活性。因此,動態(tài)語言在語言層次上集成了許多操作方便、性能良好、高度抽象的數(shù)據(jù)類型,為編程人員提供了高效的數(shù)據(jù)結(jié)構(gòu)實現(xiàn),有助于提高開發(fā)效率。以Python語言為例。Python語言提供了強大的數(shù)學(xué)運算,可以處理超大的整數(shù)和浮點數(shù),支持復(fù)數(shù)的基本運算。Python內(nèi)置了功能強大、使用方便的數(shù)組類型,程序員不需要預(yù)先聲明使用數(shù)組的大小就可以直接使用數(shù)組,也不必?fù)?dān)心數(shù)組大小不夠,這些解釋器會自動根據(jù)具體地操作動態(tài)地分配數(shù)組的存儲空間,不象C/C++中使用數(shù)組首先必須聲明數(shù)組的大小,這就使得很多人在使用數(shù)組時不知道究竟應(yīng)將大小設(shè)置成多少,大了覺得浪費,小了又擔(dān)心不夠,想改成指針方式動態(tài)分配又有可能給程序帶來新的問題。對數(shù)組類型還有一系列靈活的操作,比如可以在數(shù)組任何位置插入元素,可以對數(shù)組分片,可以將數(shù)組的某一片作為參數(shù)傳遞給某個函數(shù)而并不需要將數(shù)組從頭到尾的都傳遞。在Python的具體應(yīng)用中使用可能最廣當(dāng)屬字典類型(Dictionary Type)。字典類型是Python內(nèi)置的高效強大的數(shù)據(jù)類型,是一種類似于關(guān)聯(lián)表和哈希表的結(jié)構(gòu),Python的實現(xiàn)者花費了大量的時間對字典類型做了優(yōu)化,以盡可能保證語言的高效性。Python內(nèi)部實現(xiàn)也使用了大量的字典類型。如果沒有提供這些內(nèi)置的數(shù)據(jù)類型,開發(fā)人員可能需要獨立開發(fā)出支持所需的數(shù)據(jù)類型的庫程序,而開發(fā)這樣庫不但費時,而且在性能優(yōu)化和接口通用性方面欠佳。正是因為動態(tài)語言內(nèi)置了豐富的數(shù)據(jù)類型,節(jié)省了開發(fā)人員獨立實現(xiàn)這些數(shù)據(jù)類型的時間,從而提高了程序的開發(fā)效率[4]。
3)無類型化,用更少的代碼可完成同樣的工作
動態(tài)語言的動態(tài)類型使得編寫程序時不需要聲明變量的類型,變量無類型化省去了程序代碼中大量的編譯器編譯時所需的類型信息語句,使程序看上去簡潔明了。
比如在Tcl語言中,如命令:
button .b -text Hello! -font {Times 16} -command {puts hello}
創(chuàng)建了一個新的按鈕來顯示16點Times字體,當(dāng)用戶敲擊控制鍵時顯示一段小信息。它把六種不同事件類型混合成一條語句:一個命令名(button)、一個按鈕控制(.b)、所有權(quán)名字(-text, -font, 和-command),簡單字符串(Hello! 和hello),包含字樣名(Times)及字點大小(16)的字體名(Times 16)和Tcl腳本(puts hello)。Tcl統(tǒng)一用字符串表示了這些類型。
上述例子在Java中要調(diào)用兩個方法完成,需要7行代碼。用C++和微軟基本類(MFC)需要三個過程,25行代碼,在微軟基本類中僅僅設(shè)置字體就需要幾行代碼:
CFont *fontPtr = new CFont();
fontPtr->CreateFont(16, 0, 0,0,700, 0, 0, 0, ANSI_CHARSET,
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
DEFAULT_PITCH|FF_DONTCARE, Times New Roman);
buttonPtr->SetFont(fontPtr);
大部分代碼是由靜態(tài)類型造成的。為了設(shè)置按鈕字體,必須運用SetFont方法,但這個方法必須通過指針傳給CFont對象,這反過來需要聲明和初始化一個新的對象.為了初始化CFont對象必須喚醒它的CreateFont 方法,但CreateFont有一個需要14個特殊化引數(shù)的固定接口。在Tcl中字體(Times鉛字,16點)的基本特征不用聲明或轉(zhuǎn)換就可以立即使用。另外,Tcl允許在創(chuàng)建按鈕的命令中直接包含按鈕行為,而C++和Java中需要把它放在單獨聲明的方法中。
高級程序設(shè)計語言的高級性在于它的每一條代碼可以完成更多的工作,完成同樣的工作,使用的代碼愈少,表明語言的等級愈高。每行匯編代碼平均可以翻譯成1-3條機器指令,與此相比,每行C/C++代碼平均翻譯成5條機器指令,而對于Python、Perl、Tcl這樣的動態(tài)語言,每行代碼可翻譯成100-1000行機器指令。匯編程序中,基本上是沒有類型,所有的數(shù)據(jù)看起來都是一樣,機器的每個細(xì)節(jié)都暴露在代碼中,程序員必須顯式地管理寄存器和內(nèi)存分配、棧調(diào)用等等,所有的操作都涉及到最底層的設(shè)備操作,對程序員的專業(yè)素質(zhì)要求高,用匯編語言開發(fā)程序效率低下,因此難以編寫和維護大型程序。到高級語言出現(xiàn)后,編譯器隱藏了機器的底層細(xì)節(jié),能夠自動管理存儲分配、目標(biāo)代碼生成和優(yōu)化、過程調(diào)用順序等操作,有了抽象數(shù)據(jù)類型,將程序員從面向機器的復(fù)雜中解脫出來,提高了程序的開發(fā)效率。
動態(tài)語言把許多工作都交給解釋器去完成,程序員專注于自己需要解決的問題,建立問題解決方案的邏輯就夠了,因此更接近于自然語言。比起靜態(tài)語言,動態(tài)語言的一條語句可以完成更多的功能。從這個意義上說,動態(tài)語言是一種更高級的語言。因此,動態(tài)語言具有更高的開發(fā)效率。
支持動態(tài)重構(gòu)
隨著軟件應(yīng)用面臨的問題多樣化和復(fù)雜化,有些應(yīng)用領(lǐng)域中逐漸出現(xiàn)這樣一類需求:要求軟件在運行時還能夠改變,即動態(tài)重構(gòu)。比如在一個銀行系統(tǒng)中,要求軟件一旦運行,就不能停下來,否則會對銀行造成災(zāi)難性的損失,那么這樣的系統(tǒng)中如果在運行時發(fā)現(xiàn)某個錯誤怎樣修正呢?顯然,用靜態(tài)語言很難實現(xiàn),因為靜態(tài)語言在運行時不能修改自身代碼,用靜態(tài)語言實現(xiàn)的系統(tǒng)執(zhí)行的是經(jīng)過翻譯處理后的可執(zhí)行代碼,要修正軟件必須要經(jīng)過修改源代碼->編譯->連接->生成可執(zhí)行程序的步驟。而動態(tài)語言比較容易實現(xiàn),動態(tài)語言是解釋執(zhí)行的,運行時還能夠修改自身的代碼。國外有人用Smalltalk實現(xiàn)了這樣一個分布式的銀行系統(tǒng),銀行系統(tǒng)留有一個接口,開發(fā)好的系統(tǒng)發(fā)現(xiàn)bug后,維護人員可以通過該接口遠(yuǎn)程登錄后,可以在系統(tǒng)不停止運行的情況下把bug消除掉。
膠水語言(Glue Language)
動態(tài)語言一個廣泛的應(yīng)用就是作為“膠水語言”膠合用靜態(tài)語言編寫的組件,從而整合成一個應(yīng)用程序,這種應(yīng)用稱之為混合式應(yīng)用。RedHat Linux的安裝程序就是通過Python膠合各個組件模塊實現(xiàn)的。靜態(tài)語言開發(fā)的程序運行速度快,但開發(fā)周期較長,而動態(tài)語言靈活簡潔,開發(fā)效率可提高5-10倍,但程序運行慢,混合式應(yīng)用合理結(jié)合各自的優(yōu)點,充分利用不同語言的各自優(yōu)勢,取長補短,可以快速高效地構(gòu)建應(yīng)用程序并具有相當(dāng)?shù)男阅堋8]
這里介紹動態(tài)語言具有靜態(tài)語言所沒有的優(yōu)點并不是意味著動態(tài)語言要替代或推翻靜態(tài)語言,而是作為靜態(tài)語言有力的補充。當(dāng)然,動態(tài)語言不能回避的就是它與生俱來的缺陷,比如執(zhí)行效率低下、對大型軟件開發(fā)的支持力度不夠等等,使得動態(tài)語言難以成為主流的開發(fā)語言?,F(xiàn)代軟件面臨的問題越來越復(fù)雜,軟件工程領(lǐng)域中沒有“銀彈”可用,同樣,也沒有一種語言可以成為軟件開發(fā)的“萬金油”,可以很好地解決各類問題,每種語言都各有所長,也各有所短。動態(tài)語言和靜態(tài)語言相互補充,相得益彰,揚長避短,才能充分發(fā)揮不同語言的優(yōu)勢,在軟件開發(fā)中做到“動中有靜,靜中有動,動靜結(jié)合,各司其職”。
3.3 動態(tài)語言的發(fā)展
近幾年來,動態(tài)語言的發(fā)展勢頭良好,受到越來越多的關(guān)注,開發(fā)社區(qū)顯得十分活躍。尤其是Internet的發(fā)展,為動態(tài)語言提供一個良好的展現(xiàn)個性的活動舞臺。Perl因為編寫CGI而變得流行,VBScript和JavaScript因為能編寫網(wǎng)頁而在互聯(lián)網(wǎng)上廣泛使用。
目前,動態(tài)語言已經(jīng)進軍當(dāng)前主流的Java VM/.Net平臺。Jython是Python語言在Java VM上的完全實現(xiàn),是用純Java語言編寫的,可以輕松的訪問所有的Java類庫。Groovy也是Java VM上一款非常流行的動態(tài)語言,并作為JSR草案正式提交給標(biāo)準(zhǔn)化組織審議。IronPython是.Net平臺上Python的高效實現(xiàn)。
動態(tài)語言的應(yīng)用已經(jīng)深入到系統(tǒng)腳本、服務(wù)器程序、游戲開發(fā)、數(shù)據(jù)庫引擎和CAD系統(tǒng)的開發(fā)中,我們有理由相信,隨著Internet的發(fā)展趨勢和混合式應(yīng)用的擴展,計算機硬件的快速發(fā)展,動態(tài)語言將會展現(xiàn)蓬勃的生命力,與靜態(tài)語言一同構(gòu)建強勁的編程環(huán)境,在軟件開發(fā)中扮演越來越重要的角色。
4 Python語言簡介
Python是一種解釋型編程語言,類似Java,源代碼必須首先由編譯器轉(zhuǎn)換成字節(jié)碼(byte code),然后再由解釋器來執(zhí)行字節(jié)碼。與Java不同的是,Python的編譯器和解釋器都是一個程序。因此,源代碼也可以直接交給這個編譯器/解釋器來執(zhí)行。實際上,源代碼編譯成了字節(jié)碼,只是沒有存在硬盤上,而是直接執(zhí)行了。某些情況下,這種方式要比Java的“編輯-編譯-修改-再編譯-執(zhí)行”方式效率要高,尤其是在編寫寫一些小規(guī)模的程序時。Python程序的結(jié)構(gòu)、設(shè)計與使用方法和“主流”的計算機編程語言相當(dāng)接近。這個特點降低了門檻,易于初學(xué)者上手。
Python是一種面向?qū)ο蟮木幊陶Z言。所有的內(nèi)置(built-in)數(shù)據(jù)類型都是類:整數(shù)(int)、浮點(float)、串(string)、布爾(boolean)、元組(tuple),字典(dictionary)等等。Python的字符串常量也是其類型的實例,也就是對象,因此可以出現(xiàn)類似這樣的代碼:idx = "book.txt".find("txt")。和常規(guī)的面向?qū)ο笳Z言一樣,Python支持封裝和繼承。Python的類機制是C++ 和Modula-3 類機制的混合。在Python里,類的成員和方法都是默認(rèn)是公有的,所有方法都是虛擬的,子類可以聲明同名的方法重載父類中的方法。如果變量名是由連續(xù)兩個下劃線起頭的,比如:__count,__copy_by_size等等,編譯器在生成字節(jié)碼時會把類型名加上,比如:__Employee_count。這通常被叫做“mangle”。其實,Python的“私有”成員更多是防止與子類的成員產(chǎn)生名字沖突,而不是試圖控制對類成員和類方法的存取。在實際使用中,這種控制是很弱的,可以被輕易繞過。
Python沒有“被保護”(protected)成員的概念。在實際使用中,程序員通常在變量名前加上一個下劃線作為一種提示。這只是一種慣例,語言規(guī)則本身和編譯器并不強制執(zhí)行。
Python的很多地方都把下劃線放在特殊位置上。在子類中可以定義非私有成員和方法重載父類中同名的成員和方法。不同的是,在定義類方法時,對象實例(C++中的this)必須被顯式地放在方法形參表的第一個,Python程序的慣例是用self。在C++中,這個對象實例是被編譯器自動加上去的。
在Python類定義中,類構(gòu)造器必須用 __init__命名,析構(gòu)器必須用 __del__命名。Python類的構(gòu)造器不會自動掉用父類的構(gòu)造器,子類必須在其構(gòu)造器中顯式地調(diào)用父類的構(gòu)造器。子類可以選擇調(diào)用父類構(gòu)造器的時機,比C++和Java要靈活一些。相應(yīng)地,子類的析構(gòu)器也需要顯式地調(diào)用父類的析構(gòu)器。
Python程序中的變量是動態(tài)類型,沒有固定的類型。Python程序中的變量也不用事先聲明,變量的類型可以按需改變。這在寫小程序的時候很方便,但是對于大一些的項目,特別是需要多個人合作完成的軟件,就會帶來麻煩,因為沒有編譯器來幫你發(fā)現(xiàn)使用未聲明的變量,或是變量類型不符。通常的做法是在使用變量前檢查其是否為“空”(None),因為第一次使用的變量總是指向“空”這個特殊的對象,直到被賦值。當(dāng)然,這種檢查對性能有一定的影響。
Python使用引用計數(shù)來簡化內(nèi)存管理,程序員基本上不用關(guān)心內(nèi)存管理的問題,但是要注意避免循環(huán)引用。變量生成時,其所指對象的引用計數(shù)為一。每次變量出現(xiàn)在等號的右邊,或者出現(xiàn)在方法或函數(shù)調(diào)用實參表里,其所指對象的引用計數(shù)加一。當(dāng)一個變量的生命周期結(jié)束時,其所指對象的引用計數(shù)會被減一。如果計數(shù)值為零,就會調(diào)用對象的析構(gòu)器清除對象的存儲空間。
Python有限度地支持運算符重載。比如說,可以為一個用戶自定義的矩陣類型重載加、減、乘、除,那么使用這個矩陣類型的代碼就可以用更加直觀的+、-、*、/等操作符來直接進行矩陣的算術(shù)運算。
Python還支持結(jié)構(gòu)化的編程方式,有條件判斷、循環(huán)、函數(shù)等常見的控制結(jié)構(gòu)。一個程序可以寫出來完全沒有類的定義,從頭到尾都是free functions和函數(shù)調(diào)用。這點上,Python和C++類似。Python的函數(shù)和類方法都支持重載。因為變量是沒有類型的,只能靠區(qū)分形參數(shù)目來分辨重載的操作。
Python支持異常。和C++、Java一樣,Python的異常使得當(dāng)前棧幀中所有的變量退出作用域,同時中斷程序的執(zhí)行,將異常升級。這點上和C++及Java類似。
最有爭議性的是Python對源代碼格式的要求。不像C家族的編程語言,Python不是自由格式的。Python的作用域是靠行首縮進來界定的,而不是匹配的括號。比方說,如果一個類的定義起始于第一列,那么,類中所有成員及方法必須出現(xiàn)在第一列以后,并且處于同一層次的語句必須出現(xiàn)在相同的列上。這個特點的初衷是為了維護程序的可讀性,也確實達(dá)成了目的。大部分的Python源代碼都是排列得整整齊齊的,風(fēng)格基本接近。
每一個Python源代碼文件可以包含一個或多個的類、自由函數(shù)。多個源文件在一個文件系統(tǒng)目錄下可以成為一個模塊,只要這個目錄中有一個名為__init__.py的文件存在。這個文件甚至可以是空的。模塊可以被其他Python代碼導(dǎo)入,用類似于Java的“import graphic.2D.text”。模塊也是Python最常見的代碼重用形式。Python的編譯器和解釋器會在缺省的和指定的路徑中搜索被引入的模塊。
除了可以用Python語言自身外,其他語言也可以用來寫模塊,這體現(xiàn)了Python很強的可擴展性。最常見的是C和C++。其實,Python的很多基本模塊就是用C寫成的。通常來說,C寫成的模塊要比Python寫的快很多,通常是幾個數(shù)量級的區(qū)別。另外,很多的已有的C/C++動態(tài)庫也可以通過這種辦法成為Python的模塊。這樣,既增加了Python的應(yīng)用范圍,又把Python的易于使用和C/C++的高效、高性能就很好地結(jié)合起來了,這種混合式的編程方式越來越廣泛。這大概就是Python日益流行的原因之一。
隨著Python編譯器/解釋器一起發(fā)行的有上百個模塊,涵蓋了從字符串匹配,xml parsing,操作系統(tǒng)功能到電子郵件處理等等各個領(lǐng)域。這些模塊大部分是由來自世界各地的Python使用者貢獻(xiàn)的,在早期也沒有比較正式的命名規(guī)范,每個人都有自己的風(fēng)格。對于習(xí)慣了Java嚴(yán)謹(jǐn)命名規(guī)范的人來說,Python看起來就太不“專業(yè)”了。
此外,Python還可以被用作嵌入式的解釋器。Python的運行時環(huán)境有很清晰的C接口,整個編譯器/解釋器可以被很容易地嵌入到C或者C++的程序中,加上上面所說的由Python到C的接口,Python就具有了類似微軟的VBA的能力。這對大型的軟件系統(tǒng)是很有吸引力的。OpenOffice就有一個Python接口。
在2.2版本發(fā)布后,Python語言本身的發(fā)展逐漸轉(zhuǎn)向了對大型軟件項目的支持,添加了一些新的語言特性,比如generator、類方法、靜態(tài)方法、property、method decorator等等。Python community同時也非常注意借鑒其他編程語言的優(yōu)點。
5、動態(tài)語言與設(shè)計模式
設(shè)計模式是一個抽象層次,描述了在一個特定的環(huán)境中用來解決一般設(shè)計問題的對象和類之間的交互關(guān)系,其主要目的是充分利用語言的特性,設(shè)計可復(fù)用的、能夠適應(yīng)需求變更的軟件[9]。設(shè)計模式是一種設(shè)計思想,語言是實現(xiàn)思想的工具。因此,不同語言的特性影響了設(shè)計模式的實現(xiàn),有些語言更容易實現(xiàn)設(shè)計模式,而有些語言則比較難。GoF在設(shè)計模式一書中選用了兩種面向?qū)ο笳Z言—C++和Smalltalk實現(xiàn)軟件開發(fā)中常用的23種設(shè)計模式,其中C++為主,Smalltalk為輔,重點突出了兩種語言不同的語言特性對實現(xiàn)設(shè)計模式的影響。C++語言的運行時多態(tài)性的基礎(chǔ)是虛函數(shù)機制,指向基類的指針可以指向它的任何派生類,在實現(xiàn)設(shè)計模式時充分利用了C++這一特性,結(jié)合繼承機制,建立類和對象的層次關(guān)系,使C++最大程度的具有動態(tài)特性,將綁定關(guān)系盡可能推遲到運行時確定。
在GoF的23種模式中,部分設(shè)計模式是專門為靜態(tài)語言提出的,有些模式在動態(tài)語言中語言一級就提供直接的支持,如Command模式,動態(tài)語言提供的函數(shù)式編程將函數(shù)本身看作是類對象。
Python是一種完全面向?qū)ο蟮膭討B(tài)語言,提供了與傳統(tǒng)面向?qū)ο笳Z言截然不同的對象模型,影響了設(shè)計模式的實現(xiàn)和使用。Python中類也是對象,類和類的對象都有可供操作的特殊屬性,在運行時還可以修改類的結(jié)構(gòu)和定義,這些特性使Python具有強大的“內(nèi)省”能力,利用這種能力程序員可以創(chuàng)建高級的、動態(tài)的和靈活的應(yīng)用程序,可以更容易實現(xiàn)設(shè)計模式。本部分選取了幾種常見的設(shè)計模式,嘗試用Python語言實現(xiàn),并與C++的實現(xiàn)方式進行比較,進一步體現(xiàn)動態(tài)語言中的“動態(tài)性”及其具體應(yīng)用。
5.1 抽象工廠(Abstract Factory)
抽象工廠模式提供了一個不需要指定具體類就可以創(chuàng)建一系列相互關(guān)聯(lián)或相互依賴的對象的接口。抽象工廠隔離了具體類,客戶代碼只需通過抽象接口創(chuàng)建對象,不需要訪問具體的類。參考GoF的設(shè)計模式一書,對書中實現(xiàn)迷宮工廠的C++代碼用Python實現(xiàn)如下:
class MazeFactory:
def MakeMaze(self):
return Maze()
def MakeWall(self):
return Wall()
def MakeRoom(self, n):
return Room(n)
def MakeDoor(self, r1, r2):
return Door(r1, r2)
上述代碼定義了一個可以創(chuàng)建Maze、Wall、Room和Door的MazeFactory接口,接下來創(chuàng)建一個魔法迷宮工廠EnchantedFactory,EnchantedFactory繼承于MazeFactory,并通過MakeRoom和MakeDoor接口創(chuàng)建了具有富有個性的EnchantedRoom和EnchantedDoor。
class EnchantedFactory(MazeFactory):
def MakeRoom(self, n):
return EnchantedRoom(n)
def MakeDoor(self, r1, r2):
return EnchantedDoor(r1, r2)
這段代碼只是對C++代碼的簡單翻譯,沒有運用Python的語言特色。從上述的代碼中可以看出,抽象工廠難以向MazeFactory中添加新的產(chǎn)品,假如迷宮中還需要創(chuàng)建陷阱(Trap),就必須在MazeFactory接口中增加MakeTrap方法,這樣就造成了MazeFactory接口的不穩(wěn)定,繼承MazeFactory的所有子類的接口也隨著基類的接口改變而改變。
工廠方法(Factory Method)解決了通過引入一個的Make操作將創(chuàng)建所有產(chǎn)品類型的操作統(tǒng)一化,Make操作中有一個參數(shù)可以唯一標(biāo)識創(chuàng)建對象的類型。然而,用C++語言實現(xiàn)的工廠方法仍然存在局限性,這種局限性不利于構(gòu)建可復(fù)用的軟件。因為創(chuàng)建所有的產(chǎn)品類型都是通過Make接口的,為了保持Make接口的返回值對所有產(chǎn)品的兼容性,就不得不迫使所有產(chǎn)品類型必須繼承于一個公共的基類,然后Make接口返回該基類,這樣保證了Make返回的類型都可以轉(zhuǎn)換成特定的產(chǎn)品類型。但是,同一系列不同類型的產(chǎn)品在邏輯上可能不存在明確的公共基類,比如MazeFactory中的Maze和Wall,而且,使用公共基類導(dǎo)致了大量的向下強制轉(zhuǎn)換,這種轉(zhuǎn)換往往是不安全的,有時還不可行。[9]Pyhon語言的動態(tài)類型特性為解決該問題提供良好的方案,Python允許一個變量在運行時綁定到不同類型的對象上,所以不必要求不同類型的產(chǎn)品具有公共基類,Make接口不必聲明其返回類型,調(diào)用時具體的返回值類型在運行時交給解釋器去完成。Python實現(xiàn)工廠方法的代碼如下:
class Maze:…
class Wall:…
…
class MazeFactory(object):
def make(self, typename, *args):
if typename == ‘maze‘: return Maze()
elif typename == ‘wall‘: return Wall()
elif typename == ‘room‘:
return Room(args[0])
elif typename == ‘door‘: ]
return Door(args[0], args[1])
self是MazeFactory實例對象的引用參數(shù),typename標(biāo)識創(chuàng)建對象的類型,*args是創(chuàng)建具體對象時所需的參數(shù)列表。魔法迷宮的代碼
class EnchantedFactory(MazeFactory):
def make(self, typename, *args):
if typename == ‘room‘: return EnchantedRoom(args[0] )
elif typename == ‘door‘: return EnchantedDoor(args[0],args[1])
else: return super(EnchantedFactory, self).make(typename, args)
make方法中的return super(EnchantedFactory, self).make(typename, args)表示調(diào)用父類的操作創(chuàng)建其它類型的對象。
那么創(chuàng)建一個具體的EnchantedFactory實例的代碼:
mf = EnchantedFactory()
mz = mf.make(‘maze‘)
r1 = mf.make(‘room‘, 1)
r2 = mf.make(‘room‘, 2)
dr = mf.make(‘door‘, r1, r2)
當(dāng)需要在MazeFactory添加一個Trap新類型時,只需要在Make方法中添加標(biāo)示新類型的參數(shù)即可:
elif typename == “trap”: return Trap()
這種做法不但保持了MazeFactory對外接口的穩(wěn)定性,而且不需要類型的向下轉(zhuǎn)換。但這里同樣存在一個問題:每添加一個新類型,都要修改Make的實現(xiàn)代碼。能不能不用修改Make的代碼即可添加一個新類型呢?原型模式(Prototype)提供了一種更好的解決方案——編制產(chǎn)品字典。
5.2 原型模式(Prototype)
原型模式使用一個原型實例指定創(chuàng)建對象的類型,并且通過復(fù)制原型創(chuàng)建新的對象。原型模式的優(yōu)點是可以在運行時動態(tài)的增加和刪除產(chǎn)品類型,減少了子類化,還可以動態(tài)的配置應(yīng)用程序。使用原型管理器(Prototype Manager)可以方便實現(xiàn)運行時類型的增加和刪除,管理器中有個類型的注冊表,注冊表是個關(guān)聯(lián)存儲結(jié)構(gòu)的表,對于給定類型的鍵值可以唯一確定一個類型,增加一個新類型時就是在表中注冊該類型,客戶程序在使用一個類型前先訪問注冊表檢索它的原型。實現(xiàn)迷宮MazeFactory原型的Python代碼如下:
class MazeFactory:
def __init__(self):
self.index = {‘maze‘: Maze,
‘wall‘: Wall,
‘room‘: Room,
‘door‘: Door}
def make(self, typename, *args):
return apply(self.index[typename], args)
def registtype(self, typename, instance):
self.index[typename] = instance
def unregisttype(self, typename):
del self.index[typename]
在MazeFactory中,數(shù)據(jù)成員self.index={…}是個字典類型,存放MazeFactory產(chǎn)品類型,方法registtype和unregisttype實現(xiàn)了產(chǎn)品類型的動態(tài)增加和刪除,參數(shù)instance表示需要添加或刪除類型的實例名。假如創(chuàng)建了一個MazeFactory實例mf=MazeFactory(),實例Trap的定義如下:
class Trap:
def __init__(self, radius, height):
self.radius = radius
self.height = height
…
向mf中添加實例Trap的代碼:mf.registtype(‘trap’,Trap),而相應(yīng)的刪除代碼為mf.unregisttype(‘trap’,Trap)。
顯然,這種實現(xiàn)方式便于動態(tài)管理類型,具有良好的可擴展性。
5.3 單件模式(Singleton)
單件模式提供了一種將系統(tǒng)中類的實例個數(shù)限制為一個的機制,保證了一個類只有一個實例,并提供了該實例的一個全局訪問點。程序的不同模塊通常會共享同一個對象。單件模式隱藏了實際的全局變量,對外提供了訪問的接口,是一種很好的訪問全局變量的方法。首先我們來看一下C++是怎樣實現(xiàn)單件模式的。Singleton類的定義如下:
class Singleton {
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* _instance;
};
//對應(yīng)的實現(xiàn):
Singleton* Singleton::_instance = 0;
Singleton* Singleton::Instance ()
{
if (_instance == 0)
{
_instance = new Singleton;
}
return _instance;
}
在單件類中,靜態(tài)數(shù)據(jù)成員_instance指向已經(jīng)創(chuàng)建的實例,靜態(tài)成員函數(shù)Instance()為單件類提供了全局訪問點,客戶程序只能通過Instance()接口創(chuàng)建單件類,如果_instance不為0,則直接返回已創(chuàng)建的實例,注意Singleton類的構(gòu)造函數(shù)聲明為protected屬性防止客戶程序不通過Instance()接口創(chuàng)建它,保證了單件類的唯一性。
Python語言中的類和函數(shù)的定義可以在運行時改變,借助這一語言特性給出實現(xiàn)Singleton模式Python版本:
class Singleton(object):
def __new__(cls):
cls.instance = object.__new__(cls)
cls.__new__ = cls.Instance
cls.instance.init()
return cls.instance
def Instance(cls,type):
return cls.instance
Instance = classmethod(Instance)
def init(self):
pass
Singleton類重載了object的內(nèi)置方法__new__,object是Python中所有數(shù)據(jù)類型的基類,包括內(nèi)置數(shù)據(jù)類型和用戶自定義類型,所有的數(shù)據(jù)類型都繼承了object的屬性和方法。在Python中,類方法和靜態(tài)方法是兩個不同的概念。類的靜態(tài)方法相當(dāng)于C++中的靜態(tài)成員函數(shù),調(diào)用方式也類似。而類方法隱式地將類本身作為第一個參數(shù),在聲明和調(diào)用的格式方面與靜態(tài)方法都不同。而__new__方法是類的一個靜態(tài)方法,不是一個類方法,它在調(diào)用類的初始化方法__init__之前調(diào)用,創(chuàng)建對象的第一步就是調(diào)用__new__方法。__new__方法的第一個參數(shù)必須是一個類,并返回該類的一個新的實例,其余的參數(shù)是調(diào)用__init__所需的參數(shù)。[11] getInstance是Singleton類的類方法,定義了Singleton實例的全局訪問點。在重載的__new__方法中,首先調(diào)用了父類object的__new__方法返回一個新的Singleton的實例cls.instance,參數(shù)cls實際是對Singleton類本省的引用,instance是類的一個數(shù)據(jù)成員,保存了當(dāng)前的單件對象。語句:
cls.__new__ = cls.getInstance是將getInstance賦給__new__方法,執(zhí)行后,Singleton類的__new__方法變成了getInstance。第一次創(chuàng)建Singleton實例對象時,調(diào)用__new__方法生成Singleton的一個新的實例,試圖再次創(chuàng)建Singleton實例對象時,調(diào)用的__new__的方法實際上被“偷偷的“調(diào)包成getInstance,__new__方法的代碼不再被執(zhí)行,而是執(zhí)行g(shù)etInstance方法返回已經(jīng)創(chuàng)建的實例對象cls.instance,從而保證了只存在一個Singleton實例對象。cls.instance.init()說明了__new__方法在__init__之前調(diào)用,為了進一步初始化Singleton子類。
Singleton的子類繼承了Singleton的__new__方法,每個子類也是單件的,只能有一個實例對象,無論調(diào)用多少次構(gòu)造函數(shù),子類重載了init方法,例如:
>>> class MySingleton(Singleton):
def init(self):
print "call init..."
def __init__(self):
print "call __init__..."
print "Initilizing My singleton"
>>> a = MySingleton() #創(chuàng)建一個新的MySingleton實例對象
call init... #調(diào)用父類Singleton的__new__方法
call __init__... #調(diào)用自己的__init__方法
Initilizing My singleton
>>> b = MySingleton() #試圖再創(chuàng)建一個MySingleton對象
call __init__... #沒有調(diào)用__init__方法,說明__new__被“調(diào)包”
Initilizing My singleton
>>> a == b #a和b實際上指向同一個實例對象
True
在C++中子類化Singleton相對比較麻煩,不能直接繼承Singleton將子類定義為單件類,有三種方法創(chuàng)建Singleton的子類:在Singleton的Instance操作中設(shè)置一個參數(shù)指定需要創(chuàng)建的單件;將Instance操作從父類分離到它的各個子類中;使用單件注冊表。這些方法都可以子類化單件類,但沒有Python版本來得直觀。Python中利用了在運行時可以改變類定義的動態(tài)特性,在運行時將自身的靜態(tài)方法__new__改成另外一個方法getInstance,這種“偷梁換柱”的做法確實為某些應(yīng)用帶來了便利。
5.3 代理模式(Proxy)
從面向?qū)ο笤O(shè)計的角度看,限制訪問屬性給一些舊問題提供了一種新的解決辦法。代理模式就是一個很好的例子。代理模式用于隔離對象和訪問它的客戶,比如引用計數(shù)、不同等級的授權(quán)訪問以及對象的惰性賦值等。代理模式的結(jié)構(gòu)如下:
客戶程序不需要直接訪問實際的對象,換句話說,代理替代了實際的對象,客戶通過代理去訪問實際的對象。在C++中,這就意味著Proxy和RealSubject必須要有一個公共的基類。在Python中,通過提供相同的方法接口,Proxy可以達(dá)到冒充Subject的效果。以下Python代碼中Proxy類是基于小型的通用包裝類,它的主要功能就是為多個特定代理的實現(xiàn)提供一個基類,在Proxy類中可以重載__gettattr__方法處理不同的方法。
#Proxy Base Class
class Proxy:
def __init__( self, subject ):
self.__subject = subject
def __getattr__( self, name ):
return getattr( self.__subject, name )
#Subject class
class RGB:
def __init__( self, red, green, blue ):
self.__red = red
self.__green = green
self.__blue = blue
def Red( self ):
return self.__red
def Green( self ):
return self.__green
def Blue( self ):
return self.__blue
# More specific proxy implementation
class NoBlueProxy( Proxy ):
def Blue( self ):
return 0
考慮以下情況:首先我們需要直接訪問RGB類的實例對象,然后使用一個通用的代理實例作為一個包裝類,最后傳遞給NoBlueProxy類:
>>> rgb = RGB( 100, 192, 240 )
>>> rgb.Red()
100
>>> proxy = Proxy( rgb )
>>> proxy.Green()
192
>>> noblue = NoBlueProxy( rgb )
>>> noblue.Green()
192
>>> noblue.Blue()
0
代理模式在Python中應(yīng)用很廣泛,Python語言提供的機制中有一些就是代理模式實現(xiàn)的,比如垃圾收集中的簡單引用計數(shù)。[10]
5.4 命令模式(Command)
用過集成開發(fā)環(huán)境的人都知道,開發(fā)基于窗口圖形界面的應(yīng)用程序時,一般要用到按鈕和菜單等控件對象響應(yīng)用戶的輸入,但在集成環(huán)境的工具箱提供的按鈕和菜單并沒有顯式地實現(xiàn)該請求,也就是按鈕和菜單不知道關(guān)于請求的操作和請求的接受者的任何信息,這些請求特定于具體應(yīng)用,只用控件的使用者才知道該由哪個對象響應(yīng)哪個操作,工具箱的設(shè)計者無法知道請求的接受者和執(zhí)行的操作。那么工具箱的設(shè)計者是如何實現(xiàn)按鈕和菜單的這種功能的呢?用Command模式。
Command模式解耦了調(diào)用操作的對象(如按鈕、菜單)和實現(xiàn)該操作的對象(如文檔)。C++利用繼承組合機制實現(xiàn)Command模式,通過定義一個帶有Execute接口的Command抽象類,特定應(yīng)用相關(guān)的Command派生于此類,在Command類的子類顯式定義接受者的對象。Command類在調(diào)用操作的對象(Invoker)和實現(xiàn)該操作的對象(Receiver)之間充當(dāng)了橋梁作用, Invoker請求某個Command類,由Command類的Execute接口執(zhí)行Receiver的具體操作并將返回結(jié)果告訴Invoker,對于Invoker根本不知道是誰執(zhí)行了該操作,也不需要知道,從而實現(xiàn)了兩者的解耦。而Python具有運行時可以改變類的結(jié)構(gòu)、函數(shù)的定義的動態(tài)特性,很簡單地就實現(xiàn)了Command模式:
class Button:
def click(self):pass
class document:
def open(self):
print "open document..."
btn = Button()
doc = document()
btn.click = doc.open
執(zhí)行btn.click()時實際上相當(dāng)于調(diào)用了doc.open()的方法,實現(xiàn)了Button和document兩者的解耦,比C++的繼承組合機制要簡單。
有時一個按鈕要求執(zhí)行一系列命令,這種宏命令(MacroCommand)在應(yīng)用中也是很常見的。Python支持lamda匿名函數(shù)定義,運用函數(shù)式編程方式也可以很簡便的實現(xiàn)MacroCommand模式。我們先來看看用C++是怎樣實現(xiàn)的。C++用一個命令列表管理命令系列,執(zhí)行宏命令實際上是遍歷一次命令列表:
class Command {
public:
virtual ~Command();
virtual void Execute() = 0;
protected:
Command();
};
class MacroCommand : public Command {
public:
MacroCommand();
virtual ~MacroCommand();
virtual void Add(Command*);
virtual void Remove(Command*);
virtual void Execute();
private:
List<Command*>* _cmds;//命令列表
};
void MacroCommand::Execute ()
{
ListIterator<Command*> i(_cmds);
for (i.First(); !i.IsDone(); i.Next()) //遍歷命令列表
{
Command* c = i.CurrentItem();
c->Execute();
}
}
在Python中,假設(shè)已經(jīng)定義了一個Button類和一個Document類,Button類有一個Click方法,Document包含Paste和Replace方法,以下代碼實現(xiàn)了點擊按鈕后同時執(zhí)行Paste和Replace操作:
doc = Document()
btn = Button()
macrcocmd = lambda cmds : map(lambda cmd:cmd(),cmds)
btn.Click = lambda: macrcocmd([doc.Paste, doc.Replace])
函數(shù)map是個Python內(nèi)置函數(shù),其作用是將列表cmds作為參數(shù)傳遞給匿名函數(shù)lambda cmd : cmd()執(zhí)行并返回一個元組作為匿名函數(shù)macrocmd的參數(shù),調(diào)用btn.Click()執(zhí)行了匿名函數(shù)macrocmd(),該函數(shù)接受元組[doc.Paste, doc.Replace] 作為它的輸入?yún)?shù),執(zhí)行doc.Paste()和doc.Replace()命令。
以上我們只選取了GoF的23種模式的4種模式,展示了如何利用Python語言的動態(tài)特性實現(xiàn)設(shè)計模式。Python的靈活性和動態(tài)性為實現(xiàn)一些不同的、優(yōu)美的解決方案提供了一個良好的基礎(chǔ)。Python的無類型化解決了靜態(tài)語言實現(xiàn)工廠方法中需要不安全的強制類型轉(zhuǎn)換等問題,既減少了程序中類的設(shè)計,又避免了靜態(tài)語言中的向下轉(zhuǎn)換問題。原型模式為創(chuàng)建不同類型的對象提供一種更好的方法,利用了Python 可以運行時改變類的定義的特性,可以動態(tài)地增刪類型,提高了程序的可擴展性。而單件模式則利用了類的靜態(tài)方法__new__以及動態(tài)改變類的結(jié)構(gòu)和定義的特點,巧妙地實現(xiàn)了限制了單件類的實例對象個數(shù)的功能。在Proxy模式中,充分體現(xiàn)了Python中同一個類的多個實例對象之間可以擁有不同的結(jié)構(gòu)以及可以監(jiān)視和限制屬性訪問的語言機制,這些機制為個實現(xiàn)多個通用類提供了一個相當(dāng)完美的解決方法。在C++中不支持匿名函數(shù),命令模式初步展現(xiàn)了Python中l(wèi)amda匿名函數(shù)在實現(xiàn)某些問題的簡便性。其他的模式,如職責(zé)鏈(Chain Of Responsibility)、策略模式(Strategy)、裝飾模式(Decorator),利用Python語言特性,也可以寫出Python的實現(xiàn)版本。
6 小 結(jié)
動態(tài)語言最主要的特點就是將大部分綁定關(guān)系推遲到程序運行時確定,更強的動態(tài)特征是動態(tài)語言與靜態(tài)語言的根本區(qū)別。動態(tài)語言的這一特性提供了一種解決問題的新思路,實現(xiàn)了許多在靜態(tài)語言中比較難甚至難以實現(xiàn)的技術(shù),但動態(tài)語言在執(zhí)行時要耗費大量的時間在確定類型的綁定關(guān)系上,因此執(zhí)行效率不如靜態(tài)語言。雖然動態(tài)語言在執(zhí)行效率方面是“先天的不足”,但它的靈活性和動態(tài)性大大提高了軟件的開發(fā)效率,對于一些小型的應(yīng)用程序,動態(tài)語言更具有得天獨厚的優(yōu)勢。但動態(tài)語言替代不了靜態(tài)語言,只有合理地結(jié)合靜態(tài)語言,才能夠發(fā)揮更強勁的威力。Python是一款個性鮮明的、面向?qū)ο蟮慕换ナ絼討B(tài)語言,利用Python語言的動態(tài)特性,可以巧妙的實現(xiàn)某些應(yīng)用,提供一種優(yōu)美的解決方案,使編寫的軟件具有更高的復(fù)用性、更強的擴展性,本文選取的幾種設(shè)計模式的實現(xiàn)就是一個例子。在面向Internet的應(yīng)用程序中,動態(tài)語言具有更廣闊的前景。動態(tài)語言今后的一個發(fā)展方向就是混合式應(yīng)用,充分發(fā)揮靜態(tài)語言和動態(tài)語言各自的優(yōu)勢,揚長避短。