在前面的章節我們已經了解了面向對象的入門知識,知道了如何定義類,如何建立對象以及如何給對象發消息。為了能夠更好的使用面向對象程式設計思想進行程式開發,我們還需要對Python中的面向對象程式設計進行更為深入的了解。
之前我們讨論過Python中屬性和方法通路權限的問題,雖然我們不建議将屬性設定為私有的,但是如果直接将屬性暴露給外界也是有問題的,比如我們沒有辦法檢查賦給屬性的值是否有效。我們之前的建議是将屬性命名以單下劃線開頭,通過這種方式來暗示屬性是受保護的,不建議外界直接通路,那麼如果想通路屬性可以通過屬性的getter(通路器)和setter(修改器)方法進行對應的操作。如果要做到這點,就可以考慮使用@property包裝器來包裝getter和setter方法,使得對屬性的通路既安全又友善,代碼如下所示。
我們講到這裡,不知道大家是否已經意識到,Python是一門動态語言。通常,動态語言允許我們在程式運作時給對象綁定新的屬性或方法,當然也可以對已經綁定的屬性和方法進行解綁定。但是如果我們需要限定自定義類型的對象隻能綁定某些屬性,可以通過在類中定義__slots__變量來進行限定。需要注意的是__slots__的限定隻對目前類的對象生效,對子類并不起任何作用。
之前,我們在類中定義的方法都是對象方法,也就是說這些方法都是發送給對象的消息。實際上,我們寫在類中的方法并不需要都是對象方法,例如我們定義一個“三角形”類,通過傳入三條邊長來構造三角形,并提供計算周長和面積的方法,但是傳入的三條邊長未必能構造出三角形對象,是以我們可以先寫一個方法來驗證三條邊長是否可以構成三角形,這個方法很顯然就不是對象方法,因為在調用這個方法時三角形對象尚未建立出來(因為都不知道三條邊能不能構成三角形),是以這個方法是屬于三角形類而并不屬于三角形對象的。我們可以使用靜态方法來解決這類問題,代碼如下所示。
和靜态方法比較類似,Python還可以在類中定義類方法,類方法的第一個參數約定名為cls,它代表的是目前類相關的資訊的對象(類本身也是一個對象,有的地方也稱之為類的中繼資料對象),通過這個參數我們可以擷取和類相關的資訊并且可以建立出類的對象,代碼如下所示。
簡單的說,類和類之間的關系有三種:is-a、has-a和use-a關系。
is-a關系也叫繼承或泛化,比如學生和人的關系、手機和電子産品的關系都屬于繼承關系。
has-a關系通常稱之為關聯,比如部門和員工的關系,汽車和引擎的關系都屬于關聯關系;關聯關系如果是整體和部分的關聯,那麼我們稱之為聚合關系;如果整體進一步負責了部分的生命周期(整體和部分是不可分割的,同時同在也同時消亡),那麼這種就是最強的關聯關系,我們稱之為合成關系。
use-a關系通常稱之為依賴,比如司機有一個駕駛的行為(方法),其中(的參數)使用到了汽車,那麼司機和汽車的關系就是依賴關系。
我們可以使用一種叫做UML(統一模組化語言)的東西來進行面向對象模組化,其中一項重要的工作就是把類和類之間的關系用标準化的圖形符号描述出來。關于UML我們在這裡不做詳細的介紹,有興趣的讀者可以自行閱讀《UML面向對象設計基礎》一書。
利用類之間的這些關系,我們可以在已有類的基礎上來完成某些操作,也可以在已有類的基礎上建立新的類,這些都是實作代碼複用的重要手段。複用現有的代碼不僅可以減少開發的工作量,也有利于代碼的管理和維護,這是我們在日常工作中都會使用到的技術手段。
剛才我們提到了,可以在已有類的基礎上建立新類,這其中的一種做法就是讓一個類從另一個類那裡将屬性和方法直接繼承下來,進而減少重複代碼的編寫。提供繼承資訊的我們稱之為父類,也叫超類或基類;得到繼承資訊的我們稱之為子類,也叫派生類或衍生類。子類除了繼承父類提供的屬性和方法,還可以定義自己特有的屬性和方法,是以子類比父類擁有的更多的能力,在實際開發中,我們經常會用子類對象去替換掉一個父類對象,這是面向對象程式設計中一個常見的行為,對應的原則稱之為裡氏替換原則。下面我們先看一個繼承的例子。
子類在繼承了父類的方法後,可以對父類已有的方法給出新的實作版本,這個動作稱之為方法重寫(override)。通過方法重寫我們可以讓父類的同一個行為在子類中擁有不同的實作版本,當我們調用這個經過子類重寫的方法時,不同的子類對象會表現出不同的行為,這個就是多态(poly-morphism)。
在上面的代碼中,我們将<code>Pet</code>類處理成了一個抽象類,所謂抽象類就是不能夠建立對象的類,這種類的存在就是專門為了讓其他類去繼承它。Python從文法層面并沒有像Java或C#那樣提供對抽象類的支援,但是我們可以通過<code>abc</code>子產品的<code>ABCMeta</code>元類和<code>abstractmethod</code>包裝器來達到抽象類的效果,如果一個類中存在抽象方法那麼這個類就不能夠執行個體化(建立對象)。上面的代碼中,<code>Dog</code>和<code>Cat</code>兩個子類分别對<code>Pet</code>類中的<code>make_voice</code>抽象方法進行了重寫并給出了不同的實作版本,當我們在<code>main</code>函數中調用該方法時,這個方法就表現出了多态行為(同樣的方法做了不同的事情)。
說明: 大家可以自己嘗試在上面代碼的基礎上寫一個簡單的撲克遊戲,例如21點(Black Jack),遊戲的規則可以自己在網上找一找。