11.1 給大家介紹物件
很多讀者此前肯定聽說過Python無處不對象,然而他們並不知道對象到底是個什麼東西。他們只是在學習的時候聽說過有物件導向程式設計……這就像學開車,你並不用理解汽車為什麼會跑,但作為賽車手,這些原理就必須要懂,因為這有助於他把車開得更好。因此,本章就向大家隆重地介紹物件!
大家之前已經聽說過封裝的概念,把亂七八糟的資料扔進清單裡邊,這是一種封裝,是資料層面的封裝;把常用的程式碼片段打包成一個函數,這也是一種封裝,是語句層面的封裝;本章學習的物件,也是一種封裝的思想,不過這種思想顯然要更先進一步:物件的來源是類比真實世界,把資料和代碼都封裝在了一起。
打個比方,烏龜就是真實世界的一個物件,那麼通常應該如何來描述這個物件呢?是不是把它分為兩部分來說?
(1)可以從靜態的特徵來描述,例如,綠色的、有四條腿,10kg重,有外殼,還有個大嘴巴,這是靜態一方面的描述。
(2)還可以從動態的行為來描述,例如說它會爬,你如果追它,它就會跑,然後你把它逼急了,它就會咬人,被它咬到了,據說要打雷才會鬆開嘴巴……它的嘴巴的重要作用不是用來咬人,是用來吃東西的,然後它還會睡覺。這些都是從行為方面進行描述的。
那如果把一個人作為物件,你會從哪兩方面來描述這個人?
對嘛,無非就是他長什麼樣?這是從外觀方面找特徵,例如,眼歪鼻子斜、胸大屁股翹,這些都是靜態的特徵。另一方面就是描述他的行為?例如,唱歌、街舞、打籃球等等。
11.2 物件=屬性+方法
Python中的物件也是如此,一個物件的特徵稱為“屬性”,一個物件的行為稱為“方法”。
如果把“烏龜”寫成代碼,將會是下邊這樣:
以上代碼定義了物件的特徵(屬性)和行為(方法),但還不是一個完整的物件,將定義的這些稱為類(Class)。需要使用類來創建一個真正的物件,這個物件就叫作這個類的一個實例(Instance),也叫實例物件(Instance Objects)。
有些讀者可能還不大理解,你可以這麼想:這就好比工廠的流水線要生產一系列玩具,是不是要先做出這個玩具的模具,然後根據這個模具再進行批量生產,才得到真正的玩具?
再舉個例子:蓋房子,是不是先要有個圖紙,但光有個圖紙你能不能住進去?顯然不能,圖紙只能告訴你這個房子長什麼樣,但圖紙並不是真正的房子。要根據圖紙用鋼筋水泥建造出來的房子才能住人,另外根據一張圖紙就能蓋出很多的房子。
好,說了這麼多,那真正的實例物件怎麼創建?
創建一個物件,也叫類的產生實體,其實非常簡單:
注意,類名後邊跟著的小括弧,這跟調用函數是一樣的,所以在Python中,類名約定用大寫字母開頭,函數用小寫字母開頭,這樣更容易區分。另外賦值操作並不是必需的,但如果沒有把創建好的實例物件賦值給一個變數,那這個物件就沒辦法使用,因為沒有任何引用指向這個實例,最終會被Python的垃圾收集機制自動回收。
那如果要調用物件裡的方法,使用點操作符(.)即可,其實我們已經用了何止千百遍:
11.3 物件導向程式設計
經過前邊的熱身,相信大家對類和物件已經有了初步的認識,但似乎還是懵懵懂懂:好像物件導向程式設計很厲害,但不知道具體怎麼用?下面通過幾個主題,嘗試給大家進一步剖析Python的類和物件。
11.3.1 self是什麼
細心的讀者會發現物件的方法都會有一個self參數,那這個self到底是個什麼東西呢?如果此前接觸過其他物件導向的程式設計語言,例如C++,那麼你應該很容易對號入座,Python的self其實就相當於C++的this指針。
這裡為了照顧大部分的初學程式設計的讀者,講解下self到底是個什麼東西。如果把類比作是圖紙,那麼由類產生實體後的物件才是真正可以住的房子。根據一張圖紙就可以設計出成千上萬的房子,它們長得都差不多,但它們都有不同的主人。每個人都只能回自己的家裡,陪伴自己的孩子……所以self這裡就相當於每個房子的門牌號,有了self,你就可以輕鬆找到自己的房子。
Python的self參數就是同一個道理,由同一個類可以生成無數物件,當一個物件的方法被調用的時候,物件會將自身的引用作為第一個參數傳給該方法,那麼Python就知道需要操作哪個物件的方法了。
通過一個例子稍微感受下:
11.3.2 你聽說過Python的魔法方法嗎
據說,Python的物件天生擁有一些神奇的方法,它們是物件導向的Python的一切。它們是可以給你的類增加魔力的特殊方法,如果你的物件實現了這些方法中的某一個,那麼這個方法就會在特殊的情況下被Python所調用,而這一切都是自動發生的。
Python的這些具有魔力的方法,總是被雙底線所包圍,今天就講其中一個最基本的特殊方法:__init__(),關於其他Python的魔法方法,接下來會專門用一個章節來詳細講解。
通常把__init__()方法稱為構造方法,__init__()方法的魔力體現在只要產生實體一個物件,這個方法就會在物件被創建時自動調用(在C++裡你也可以看到類似的東西,叫“構造函數”)。其實,產生實體物件時是可以傳入參數的,這些參數會自動傳入__init__()方法中,可以通過重寫這個方法來自訂物件的初始化操作。舉個例子:
11.3.3 公有和私有
一般物件導向的程式設計語言都會區分公有和私有的資料類型,像C++和Java它們使用public和private關鍵字,用於聲明資料是公有的還是私有的,但在Python中並沒有用類似的關鍵字來修飾。
難道Python所有東西都是透明的?也不全是,預設上物件的屬性和方法都是公開的,可以直接通過點操作符(.)進行訪問:
為了實現類似私有變數的特徵,Python內部採用了一種叫name mangling(名字改編)的技術,在Python中定義私有變數只需要在變數名或函數名前加上“__”兩個底線,那麼這個函數或變數就會成為私有的了:
這樣在外部將變數名“隱藏”起來了,理論上如果要訪問,就需要從內部進行:
但是你認真琢磨一下這個技術的名字name mangling(名字改編),那就不難發現其實Python只是動了一下手腳,把雙下橫線開頭的變數進行了改名而已。實際上在外部你使用“_類名__變數名”即可訪問雙下橫線開頭的私有變數了:
(注:Python目前的私有機制其實是偽私有,Python的類是沒有許可權控制的,所有變數都是可以被外部調用的。最後的這部分有些讀者(尤其是沒有接觸過物件導向程式設計的讀者)可能看不懂,想不明白有什麼用?沒事,先放著,下節講完繼承機制你就會豁然開朗了。)
11.4 繼承
現在需要擴展遊戲,對魚類進行細分,有金魚(Goldfish)、鯉魚(Carp)、三文魚(Salmon),還有鯊魚(Shark)。那麼我們就再思考一個問題:能不能不要每次都從頭到尾去重新定義一個新的魚類呢?因為我們知道大部分魚的屬性和方法是相似的,如果有一種機制可以讓這些相似的東西得以自動傳遞,那就方便快捷多了。沒錯,你猜到了,這種機制就是今天要講的:繼承。
語法很簡單:
class類名(被繼承的類): …
被繼承的類稱為基類、父類或超類;繼承者稱為子類,一個子類可以繼承它的父類的任何屬性和方法。舉個例子:
需要注意的是,如果子類中定義與父類同名的方法或屬性,則會自動覆蓋父類對應的方法或屬性:
好,那嘗試來寫一下剛才提到的金魚(Goldfish)、鯉魚(Carp)、三文魚(Salmon),還有鯊魚(Shark)的例子:
奇怪!同樣是繼承於Fish類,為什麼金魚(goldfish)可以移動,而鯊魚(shark)一移動就報錯呢?
其實這裡拋出的異常說得很清楚了:Shark物件沒有x屬性。原因其實是這樣的:在Shark類中,重寫了魔法方法__init__,但新的__init__方法裡邊沒有初始化鯊魚的x座標和y座標,因此調用move方法就會出錯。那麼解決這個問題的方案就很明顯了,應該在鯊魚類中重寫__init__方法的時候先調用基類Fish的__init__方法。
下面介紹兩種可以實現的技術:
調用未綁定的父類方法。
使用super函數。
11.4.1 調用未綁定的父類方法
調用未綁定的父類方法,聽起來有些高深,但大家參考下面改寫的代碼就能心領神會了:
再運行下發現鯊魚也可以成功移動了:
這裡需要注意的是這個self並不是父類Fish的實例物件,而是子類Shark的實例物件,所以這裡說的未綁定是指並不需要綁定父類的實例物件,使用子類的實例物件代替即可。
有些讀者可能不大理解,沒關係,這一點都不重要!因為在Python中,有一個更好的方案可以取代它,就是使用super函數。
11.4.2 使用super函數
super函數能夠幫我自動找到基類的方法,而且還為我們傳入了self參數,這樣就不需要做這些事情了:
運行後得到同樣的結果:
super函數的“超級”之處在於你不需要明確給出任何基類的名字,它會自動幫您找出所有基類以及對應的方法。由於你不用給出基類的名字,這就意味著如果需要改變類繼承關係,只要改變class語句裡的父類即可,而不必在大量代碼中去修改所有被繼承的方法。
11.5 多重繼承
除此之外Python還支援多繼承,就是可以同時繼承多個父類的屬性和方法:
上面就是基本的多重繼承語法。但多重繼承其實很容易導致代碼混亂,所以當你不確定是否真的必須使用多重繼承的時候,請儘量避免使用它,因為有些時候會出現不可預見的BUG。
【擴展閱讀】多重繼承的陷阱:鑽石繼承(菱形繼承)問題(http://bbs.fishc.com/thread-48759-1-1.html)。
11.6 組合
前邊先是學習了繼承的概念,然後又學習了多重繼承,但聽到大牛們強調說不到必要的時候不使用多重繼承。哎呀,這可讓大家煩惱死了,就像上回我們有了烏龜類、魚類,現在要求定義一個類,叫水池,水池裡要有烏龜和魚。用多重繼承就顯得很奇怪,因為水池和烏龜、魚是不同物種,那要怎樣才能把它們組合成一個水池的類呢?
在Python裡其實很簡單,直接把需要的類放進去產生實體就可以了,這就叫組合:
Python的特性其實還支援另外一種很流行的程式設計模式:Mixin,有興趣的朋友可以看看【擴展閱讀】Mixin程式設計機制(http://bbs.fishc.com/thread-48888-1-1.html)。
11.7 類、類物件和實例物件
先來分析一段代碼:
圖11-1 類、類物件和實例物件
從上面的例子可以看出,對實例物件c的count屬性進行賦值後,就相當於覆蓋了類物件C的count屬性。如圖11-1所示,如果沒有賦值覆蓋,那麼引用的是類物件的count屬性。
需要注意的是,類中定義的屬性是靜態變數,也就是相當於C語言中加上static關鍵字聲明的變數,類的屬性是與類物件進行綁定,並不會依賴任何它的實例物件。這點待會兒繼續講解。
另外,如果屬性的名字跟方法名相同,屬性會覆蓋方法:
為了避免名字上的衝突,大家應該遵守一些約定俗成的規矩:
類的定義要“少吃多餐”,不要試圖在一個類裡邊定義出所有能想到的特性和方法,應該利用繼承和組合機制來進行擴展。
11.8 到底什麼是綁定
Python嚴格要求方法需要有實例才能被調用,這種限制其實就是Python所謂的綁定概念。前面也粗略地解釋了一下綁定,但有些讀者可能會這麼嘗試,然後發現也可以調用:
但這樣做會有一個問題,就是根據類產生實體後的物件根本無法調用裡邊的函數:
實際上由於Python的綁定機制,這裡自動把bb對象作為第一個參數傳入,所以才會出現TypeError。
為了讓大家更好地理解,再深入挖一挖:
可以使用__dict__查看物件所擁有的屬性:
__dict__屬性是由一個字典組成,字典中僅有實例物件的屬性,不顯示類屬性和特殊屬性,鍵表示的是屬性名,值表示屬性相應的資料值。
為什麼會這樣呢?完全是歸功於self參數:當實例物件dd去調用setXY方法的時候,它傳入的第一個參數就是dd,那麼self.x=4,self.y=5也就相當於dd.x=4,dd.y=5,所以你在實例物件,甚至類物件中都看不到x和y,因為這兩個屬性是只屬於實例物件dd的。
接著再深入一下,請思考:如果我把類實例刪除掉,實例物件dd還能否調用printXY方法?
11.9 一些相關的BIF
下面介紹與類和物件相關的一些BIF(內置函數)。
1.issubclass(class, classinfo)
如果第一個參數(class)是第二個參數(classinfo)的一個子類,則返回True,否則返回False:
(1)一個類被認為是其自身的子類。
(2)classinfo可以是類物件組成的元組,只要class是其中任何一個候選類的子類,則返回True。
(3)在其他情況下,會拋出一個TypeError異常。
2.isinstance(object, classinfo)
如果第一個參數(object)是第二個參數(classinfo)的實例物件,則返回True,否則返回False:
(1)如果object是classinfo的子類的一個實例,也符合條件。
(2)如果第一個參數不是對象,則永遠返回False。
(3)classinfo可以是類物件組成的元組,只要object是其中任何一個候選物件的實例,則返回True。
(4)如果第二個參數不是類或者由類物件組成的元組,會拋出一個TypeError異常。
Python提供以下幾個BIF用於訪問物件的屬性。
3.hasattr(object, name)
attr即attribute的縮寫,屬性的意思。接下來將要介紹的幾個BIF都是跟物件的屬性有關係的,例如這個hasattr()的作用就是測試一個物件裡是否有指定的屬性。
第一個參數(object)是物件,第二個參數(name)是屬性名(屬性的字串名字),舉個例子:
4.getattr(object, name[, default])
返回物件指定的屬性值,如果指定的屬性不存在,則返回default(可選參數)的值;若沒有設置default參數,則拋出ArttributeError異常。
5.setattr(object, name, value)
與getattr()對應,setattr()可以設置物件中指定屬性的值,如果指定的屬性不存在,則會新建屬性並賦值。
6.delattr(object, name)
與setattr()相反,delattr()用於刪除物件中指定的屬性,如果屬性不存在,則拋出AttributeError異常。
7.property(fget=None, fset=None,
fdel=None, doc=None)
俗話說:條條大路通羅馬。同樣是完成一件事,Python其實提供了好幾個方式供你選擇。property()是一個比較奇葩的BIF,它的作用是通過屬性來設置屬性。說起來有點繞,看一下例子:
property()返回一個可以設置屬性的屬性,當然如何設置屬性還是需要人為來寫代碼。第一個參數是獲得屬性的方法名(例子中是getSize),第二個參數是設置屬性的方法名(例子中是setSize),第三個參數是刪除屬性的方法名(例子中是delSize)。
property()有什麼作用呢?舉個例子,在上面的例題中,為用戶提供setSize方法名來設置size屬性,並提供getSize方法名來獲取屬性。但是有一天你心血來潮,突然想對程式進行大改,就可能需要把setSize和getSize修改為setXSize和getXSize,那就不得不修改使用者調用的介面,這樣的體驗非常不好。
有了property(),所有問題就迎刃而解了,因為像上邊一樣,為使用者訪問size屬性只提供了x屬性。無論內部怎麼改動,只需要相應的修改property()的參數,用戶仍然只需要去操作x屬性即可,沒有任何影響。
很神奇是吧?想知道它是如何工作的?學完緊接著要講的魔法方法,你就知道了。
[1] 駱駝式命名法(Camel-Case)又稱駝峰命名法,是電腦程式編寫時的一套命名規則(慣例)。正如它的名稱Camel Case所表示的那樣,是指混合使用大小寫字母來構成變數和函數的名字,程式師們為了自己的代碼能更容易在同行之間交流,所以多採取統一的可讀性比較好的命名方式。

































0 留言:
發佈留言