# 第七章 更加抽象
> 來源:http://www.cnblogs.com/Marlowes/p/5426233.html
> 作者:Marlowes
前幾章介紹了Python主要的內建對象類型(數字、字符串、列表、元組和字典),以及內建函數和標準庫的用法,還有定義函數的方法。現在看來,還差一點——創建自己的對象。這正是本章要介紹的內容。
為什么要自定義對象呢?建立自己的對象類型可能很酷,但是做什么用呢?使用字典、序列、數字和字符串來創建函數,完成這項工作還不夠嗎?這樣做當然可以,但是創建自己的對象(尤其是類型或者被稱為_類_的對象)是Python的核心概念——非常核心,事實上,Python被成為_面向對象_的語言(和SmallTalk、C++、Java以及其他語言一樣)。本章將會介紹如何創建對象,以及多態、封裝、方法、特性、超類以及繼承的概念——新知識很多。那么我們開始吧。
_注:熟悉面向對象程序設計概念的讀者也應該了解構造函數。本章不會提到構造函數,關于它的完整討論,請參見第九章。_
## 7.1 對象的魔力
在面向對象程序設計中,術語_對象_(`object`)基本上可以看做數據(特性)以及由一系列可以存取、操作這些數據的方法所組成的集合。使用對像代替全局變量和函數的原因可能有很多。其中對象最重要的有點包括以下幾方面。
? 多態(Polymorphism):意味著可以對不同類的對象使用同樣的操作,它們會像被“施了魔法一般”工作。
? 封裝(Encapsulation):對外部世界隱藏對象的工作細節。
? 繼承(Inheritance):以通用的類為基礎建立專門的對象。
在許多關于面向對象程序設計的介紹中,這幾個概念的順序是不同的。封裝和繼承會首先被介紹,因為它們被用作現實世界中的對象的模型。這種方法不錯,但是在我看來,面向對象程序設計最有趣的特性是多態。(以我的經歷來看)它也是讓大多數人犯暈的特性。所以本章會以多態開始,而且這一個概念就足以讓你喜歡面向對象程序設計了。
### 7.1.1 多態
術語_多態_來自希臘語,意思是“有多種形式”。多態意味著就算不知道變量所引用的對象類型是什么,還是能對它進行操作,而它也會根據對象(或類)類型的不同而表現出不同的行為。例如,假設一個食品銷售的商業網站創建了一個在線支付系統。程序會從系統的其他部分(或者以后可能會設計的其他類似的系統)獲得一“購物車”中的商品,接下來要做的就是算出總價然后使用信用卡支付。
當你的程序獲得商品時,首先想到的可能是如何具體地表示它們。比如需要將它們作為元組接收,像下面這樣:
```
("SPAM", 2.50)
```
如果需要描述性標簽和價格,這樣就夠了。但是這個程序還是不夠靈活。我們假設網站支持拍賣服務,價格在貨物賣出之前會逐漸降低。如果用戶能夠把對象放入購物車,然后處理結賬(你的系統部分),等價格到了滿意的程度后按下“支付”按鈕就好了。
但是這樣一來簡單的元組就不能滿足需要了。為了實現這個功能,代碼每次詢問價格的時候,對象都需要檢查當前的價格(通過網絡的某些功能),價格不能固定在元組中。解決起來不難,只要寫個函數:
```
# Don't do it
def getPrice(object):
if isinstance(object, tuple):
return object[1]
else:
return magic_network_method(object)
```
_注:這里用`isinstance`進行類型/類檢查是為了說明一點,類型檢查一般來說并不是什么好方法,能不用則不用。函數`isinstance`在7.2.6節會介紹。_
前面的代碼中使用`isinstance`函數查看對象是否為元組。如果是的話,就返回它的第2個元素,否則會調用一些“有魔力的”網絡方法。
假設網絡功能部分已經存在,那么問題已經解決了,目前為止是這樣。但程序還不是很靈活。如果某些聰明的程序員決定用十六進制數的字符串來表示價格,然后存儲在字典中的鍵"price"下面呢?沒問題,只要更新函數:
```
# Don't do it
def getPrice(object): if isinstance(object, tuple): return object[1] elif isinstance(object, dict): return int(objecct["price"]) else: return magic_network_method(object)
```
現在是不是已經考慮到了所有的可能性?但是如果某些人希望為存儲在其他鍵下面的價格增加新的字典呢?那有怎么辦呢?可以再次更新`getPrice`函數,但是這種工作還要做多長時間?每次有人要實現價格對象的不同功能時,都要再次實現你的模塊。但是如果這個模塊已經賣出了并且轉到了其他更酷的項目中,那要怎么應付客戶?顯然這是個不靈活且不切實際的實現多種行為的代碼編寫方式。
那么應該怎么辦?可以讓對象自己進行操作。聽起來很清楚,但是想一下,這樣做會輕松很多。每個新的對象類型都可以檢索和計算自己的價格并且返回結果,只需向它詢問價格即可。這時候多態(在某種程度上還有封裝)就要出場了。
1\. 多態和方法
程序接收到一個對象,完全不了解該對象的內部實現方式——它可能有多種“形狀”。你要做的就是詢問價格,這樣就夠了,實現方法是我們熟悉的:
```
>>> object.getPrice()
2.5
```
綁定到對象特性上面的函數成為_方法_(method)。我們已經見過字符串、列表和字典方法。實際上多態也已經出現過:
```
>>> "abc".count("a") 1
>>> [1, 2, "a"].count("a") 1
```
對于變量`x`來說,不需要知道它是字符串還是列表,就可以調用它的`count`方法,不用管它是什么類型(只要你提供了一個字符串作為參數即可)。
讓我們做個實驗吧。標準庫`random`中包含`choice`函數,可以從序列中隨機選出元素。給變量賦值:
```
>>> from random import choice
>>> x = choice(["Hello, world!", [1, 2, "e", "e", 4]])
```
運行后,變量`x`可能會包含字符串`"Hello, world!"`,也有可能包含列表`[1, 2, "e", "e", 4]`——不用關心到底是哪個類型。要關心的就是在變量`x`中字符`e`出現多少次,而不管`x`是字符串還是列表。可以使用剛才的`count`函數,結果如下:
```
>>> x.count("e") 1
```
本例中,看來是字符串勝出了(Marlowes:原文上隨機選擇到的是字符串。 =_=)。但是關鍵點在于不需要檢測類型:只需要知道`x`有個叫做`count`的方法,帶有一個字符作為參數,并且返回整數值就夠了。如果其他人創建的對象類也有`count`方法,那也無所謂,你只需要像用字符串和列表一樣使用該對象就行了。
2\. 多態的多種形式
任何不知道對象到底是什么類型,但是又要對對象“做點兒什么”的時候,都會用到多態。這不僅限于方法,很多內建運算符和函數都有多態的性質,考慮下面這個例子:
```
>>> 1 + 2
3
>>> "Fish" + "license"
'Fishlicense'
```
這里的加運算符對于數字(本例中為整數)和字符串(以及其他類型的序列)都能起作用。為說明這一點,假設有個叫做`add`的函數,它可以將兩個對象相加。那么可以直接將其定義成上面的形式(功能等同但比`operator`模塊中的`add`函數效率低些)。
```
>>> def add(x, y):
... return x + y
# 對于很多類型的參數都可以用:
>>> add(1, 2)
3
>>> add("Fish", "license")
'Fishlicense'
```
看起來有些傻,但是關鍵在于參數可以是任何支持加法的對象(注意,這類對象只支持同類的加法。調用`add(1, "license")`不會起作用)。如果需要編寫打印對象長度消息的函數,只需要對象具有長度(`len`函數可用)即可。
```
>>> def length_message(x):
... print "The length of", repr(x), "is", len(x)
```
可以看到,函數中用了`repr`函數,`repr`函數是多態特性的代表之一,可以對任何東西使用。讓我們看看:
```
>>> length_message("Fnord")
The length of 'Fnord' is 5
>>> length_message([1, 2, 3])
The length of [1, 2, 3] is 3
```
很多函數和運算符都是多態的——你寫的絕大多數程序可能都是,即便你并非有意這樣。只要使用多態函數和運算符,就會與“多態”發生關聯。事實上,唯一能夠毀掉多態的就是使用函數顯式地檢查類型,比如`type`、`isinstance`以及`issubclass`函數等。如果可能的話,應該盡力避免使用這些毀掉多態的方式。真正重要的是如何讓對象按照你所希望的方式工作,不管它是否是正確的類型(或者類)。
_注:這里所討論的多態的形式是Python式編程的核心,也是被成為“鴨子類型”(duck typing)的東西。這個名詞出自俗語“如果它像鴨子一樣呱呱大叫······”。有關它的更多信息,請參見 http://en.wikipedia.org/wiki/Duck_typing _
### 7.1.2 封裝
_封裝_是指向程序中的其他部分隱藏對象的具體實現的原則。聽起來有些像多態,也是使用對象而不用知道其內部細節,兩者概念有些類似,因為它們都是_抽象的原則_,它們都會幫助處理程序組件而不用過多關心多余細節,就像函數做的一樣。
但是封裝并不等同于多態。多態可以讓用戶對于不知道是什么類(對象類型)的對象進行方法調用,而封裝是可以不用關心對象是如何構建的而直接進行調用。聽起來還是有些相似?讓我們用多態而不用封裝寫個例子,假設有個叫做`OpenObject`的類(本章后面會學到如何創建類):
```
>>> o = OpenObject()
# This is how we create objects...
>>> o.setName("Sir Lancelot")
>>> o.getName()
'Sir Lancelot'
```
創建了一個對象(通過像調用函數一樣調用類)后,將變量`o`綁定到該對象上。可以使用`setName`和`getName`方法(假設已經由`OpenObject`類提供)。一切看起來都很完美。但是假設變量`o`將它的名字存儲在全局變量`globalName`中:
```
>>> globalName
"Sir Lancelot"
```
這就意味著在使用`OpenObject`類的實例時候,不得不關心`globalName`的內容。實際上要確保不會對它進行任何更改:
```
>>> globalName = "Sir XuHoo"
>>> o.getName()
'Sir XuHoo'
```
如果創建了多個`OpenObject`實例的話就會出現問題,因為變量相同,所以可能會混淆:
```
>>> o1 = OpenObject()
>>> o2 = OpenObject()
>>> o1.setName("Robin Hood")
>>> o2.getName()
'Robin Hood'
```
可以看到,設定一個名字后,其他的名字也就自動設定了。這可不是想要的結果。
基本上,需要將對象進行抽象,調用方法的時候不用關心其他的東西,比如它是否干擾了全局變量。所以能將名字“封裝”在對象內嗎?沒問題。可以將其作為_特性_(attribute)存儲。
正如方法一樣,特性是作為變量構成對象的一部分,事實上方法更像是綁定到函數上的屬性(在本章的7.2.3節中會看到方法和函數重要的不同點)。
如果不用全局變量而用特性重寫類,并且重命名為`ClosedObject`,它會像下面這樣工作:
```
>>> c = ClosedObject()
>>> c.setName("Sir Lancelot")
>>> c.getName()
'Sir Lancelot'
```
目前為止還不錯。但是,值可能還是存儲在全局變量中的。那么再創建另一個對象:
```
>>> r = ClosedObject()
>>> r.setName("Sir Robin")
>>> r.getName()
'Sir Robin'
```
可以看到新的對象的名稱已經正確設置。這可能正是我們期望的。但是第一個對象怎么樣了呢?
```
>>> c.getName()
'Sir Lancelot'
```
名字還在!這是因為對象有它自己的_狀態_(state)。對象的狀態由它的特性(比如名稱)來描述。對象的方法可以改變它的特性。所以就像是將一大堆函數(方法)捆在一起,并且給予它們訪問變量(特性)的權力,它們可以在函數調用之間保持保存的值。
本章后面的“再論私有化”一節也會對Python的封裝機制進行更詳細的介紹。
### 7.1.3 繼承
繼承是另外一個懶惰(褒義)的行為。程序員不想把同一段代碼輸入好幾次。之前使用的函數避免了這種情況,但是現在又有個更微妙的問題。如果已經有了一個類,而又想建立一個非常類似的呢?新的類可能只是添加幾個方法。在編寫新類時,又不想把舊類的代碼全都復制過去。
比如說有個`Shape`類,可以用來在屏幕上畫出指定的形狀。現在需要創建一個叫做`Rectangle`的類,它不但可以在屏幕上畫出指定的形狀,而且還能計算該形狀的面積。但又不想把Shape里面已經寫好的`draw`方法再寫一次。那么該怎么辦?可以讓`Rectangle`從`Shape`類_繼承_方法。在`Rectangle`對象上調用`draw`方法時,程序會自動從`Shape`類調用該方法。(參見7.2.5節)。
## 7.2 類和類型
現在讀者可能對什么是類有了大體感覺——或者已經有些不耐煩聽我對它進行更多介紹了。在開始介紹之前,先來認識一下什么是類,以及它和類型又有什么不同(或相同)。
### 7.2.1 類到底是什么
前面的部分中,類這個詞已經多次出現,可以將它或多或少地視為_種類_或者_類型_的同義詞。從很多方面來說,這就是類——一種對象。所有的對象都屬于某一個類,稱為類的_實例_(instance)。
例如,現在請往窗外看,鳥就是“鳥類” 的實例。鳥類是一個非常通用(抽象)的類,具有很多子類:看到的鳥可能屬于子類“百靈鳥”。可以將“鳥類”想象成所有鳥的集合,而“百靈鳥類”是其中的一個子集。當一個對象所屬的類是另外一個對象所屬類的子集時,前者就被成為后者的_子類_(subclass),所以“百靈鳥類”是“鳥類”的子類。相反,“鳥類”是“百靈鳥類”的_超類_(superclass)。
_注:日常交談中,可能經常用復數來描述對象的類,比如`birds`或者`larkes`。Python中,習慣上都使用單數名詞,并且首字母大寫,比如`Bird`和`Lark`。_
這樣一比喻,子類和超類就容易理解了。但是在面向對象程序設計中,子類的關系是隱式的,因為一個類的定義取決于它所支持的方法。類的所有實例都會包含這些方法,所以所有_子類_的所有實例都有這些方法。定義子類只是個定義更多(也有可能是重載已經存在的)的方法的過程。
例如,鳥類Bird可能支持fly方法,而企鵝類`Penguin`(`Bird`的子類)可能會增加個`eatFish`方法。當創建`Penguin`類時,可能會想要重寫(`override`)超類的`fly`方法,對于`Penguin`的實例來說,這個方法要么什么也不做,要么就產生異常(參見第8章),因為`penguin`(企鵝)不會`fly`(飛)。
_注:在舊版本的Python中,類和類型之間有很明顯的區別。內建的對象是基于類型的,自定義的對象則是基于類的。可以創建類但是不能創建類型。最近版本的Python中,事情有了些變化。基本類型和類之間的界限開始模糊了。可以創建內建類型的子類(或子類型),而這些類型的行為更類似于類。在越來越熟悉這門語言后會注意到這一點。如果感興趣的話,第九章中會有關于這方面的更多信息。_
### 7.2.2 創建自己的類
終于來了!可以創建自己的類了!先來看一個簡單的類:
```
# 確定使用新式類
__metaclass__ = type
class Person:
def setName(self, name):
self.name = Name
def getName(self):
return self.name
def greet(self):
print "Hello, world! I'm %s" % self.name
```
_注:所謂的舊式類和新式類之間是有區別的。除非是Python3.0之前版本中默認附帶的代碼,否則再繼續使用舊式類已無必要。新式類的語法中,需要在模塊或者腳本開始的地方放置賦值語句`__metaclass__ = type`(并不會在每個例子中顯式地包含這行語句)。除此之外也有其他的方法,例如繼承新式類(比如`object`)。后面馬上就會介紹繼承的知識。在Python3.0中,舊式類的問題不用再擔心,因為它們根本就不存在了。請參見第九章獲取更多信息。_
這個例子包含3個方法定義,除了它們是寫在`class`語句里面外,一切都像是函數定義。`Person`當然是類的名字。`class`語句會在函數定義的地方創建自己的命名空間(參見7.2.4節)。一切看起來都挺好,但是那個`self`參數看起來有點奇怪。它是對于對象自身的引用。那么它是什么對象?讓我們創建一些實例看看:
```
>>> foo = Person()
>>> bar = Person()
>>> foo.setName("Luke Skywalker")
>>> bar.setName("Anakin Skywalker")
>>> foo.greet()
Hello, world! I'm Luke Skywalker
>>> bar.greet()
Hello, world! I'm Anakin Skywalker
```
好了,例子一目了然,應該能說明`self`的用處了。在調用`foo`的`setName`和`greet`函數時,`foo`自動將自己作為第一個參數傳入函數中——因此形象的命名為`self`。對于這個變量,每個人可能都會有自己的叫法,但是因為它總是對象自身,所以習慣上總是叫做`self`。
顯然這就是`self`的用處和存在的必要性。沒有它的話,成員方法就沒法訪問他們要對其特性進行操作的對象本身了。
和之前一樣,特性是可以在外部訪問的:
```
>>> foo.name 'Luke Skywalker'
>>> bar.name = "Yoda"
>>> bar.greet()
Hello, world! I'm Yoda
```
_注:如果知道`foo`是`Person`的實例的話,那么還可以把`foo.greet()`看作`Person.greet(foo)`方便的簡寫。_
### 7.2.3 特性、函數和方法
(在前面提到的)`self`參數事實上正是方法和函數的區別。方法(更專業一點可以成為_綁定_方法)將它們的第一個參數綁定到所屬的實例上,因此您無需顯式提供該參數。當然也可以將特性綁定到一個普通函數上,這樣就不會有特殊的`self`參數了:
```
>>> class Class:
... def method(self):
... print "I have a self!"
>>> def function():
... print "I don't..."
>>> instance = Class()
>>> instance.method()
I have a self!
>>> instance.method = function
>>> instance.method()
I don't...
```
注意,`self`參數并不依賴于調用方法的方式,前面我們使用的是`instance.method`(實例.方法)的形式,可以隨意使用其他變量引用同一個方法:
```
>>> class Bird:
... song = "Squaawk!"
... def sing(self):
... print self.song
>>> bird = Bird()
>>> bird.sing()
Squaawk!
>>> brid.song
>>> birdsong = bird.sing()
Squaawk!
```
盡管最后一個方法調用起來與函數調用十分相似,但是變量`birdsong`引用綁定方法(第九章中,將會介紹類是如何調用超類方法的(具體來說就是超類的構造器)。這些方法直接通過類調用,他們沒有綁定自己的`self`參數到任何東西上,所以叫做非綁定方法)`bird.sing`上,也就意味著這還是會對`self`參數進行訪問(也就是說,它仍舊綁定到類的相同實例上)。
**再論私有化**
默認情況下,程序可以從外部訪問一個對象的特性。再次使用前面討論過的相關封裝的例子:
```
>>> c.name 'Sir Lancelot'
>>> c.name = 'Sir Gumby'
>>> c.getName() 'Sir Gumby'
```
有些程序員覺得這樣做是可以的,但是有些人(比如SmallTalk之父,SmallTalk的對象特性只允許由同一個對象的方法訪問)覺得這樣做就破壞了封裝的原則。他們認為對象的狀態對于外部應該是_完全隱藏_(不可訪問)的。有人可能會奇怪為什么他們會站在如此極端的立場上。每個對象管理自己的特性還不夠嗎?為什么還要對外部世界隱藏呢?畢竟如果能直接使用`ClosedObject`的`name`特性的話就不用使用`setName`和`getName`方法了。
關鍵在于其他程序員可能不知道(可能也不應該知道)你的對象內部的具體操作。例如,`ClosedObject`可能會在其他對象更改自己的名字的時候,給一些管理員發送郵件消息。這應該是`setName`方法的一部分。但是如果直接使用`c.name`設定名字會發生什么?什么都沒發生,Email也沒發出去。為了避免這類事情的發生,應該使用_私有_(private)特性,這是外部對象無法訪問,但`getName`和`setName`等_訪問器_(accessor)能夠訪問的特性。
_注:第九章中,將會介紹有關屬性(property)的只是,它是訪問器最有力的替代者。_
Python并不直接支持私有方式,而是要靠程序員自己把握在外部進行特性修改的時機。畢竟在使用對象前應該知道如何使用。但是,可以用一些小技巧達到私有特性的效果。
為了讓方法或者特性變為私有(從外部無法訪問),只要在它的名字前面加上雙下劃線即可:
```
class Secretive():
def __inaccessible(self):
print "Bet you can't see me..."
def accessible(self):
print "The secret message is:"
self.__inaccessible()
```
現在`__inaccessible`從外界是無法訪問的,而在類內部還能使用(比如從`accessible`)訪問:
```
>>> s = Secretive()
>>> s.__inaccessible()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: Secretive instance has no attribute '__inaccessible'
>>> s.accessible()
The secret message is:
Bet you can't see me...
```
盡管雙下劃線有些奇怪,但是看起來像是其他語言中的標準的私有方法。真正發生的事情才是不標準的。類的內部定義中,所有以雙下劃線開始的名字都被“翻譯”成前面加上單下劃線和類名的形式:
```
>>> Secretive._Secretive__inaccessible <unbound method Secretive.__inaccessible>
```
在了解這些幕后的事情后,實際上還能在類外訪問這些私有方法,盡管不應該這么做:
```
>>> s._Secretive__inaccessible()
Bet you can't see me...
```
簡而言之,確保其他人不會訪問對象的方法和特性是不可能的,但是這類”名稱變化術“是他們不應該訪問這些函數或者特性的強有力信號。
如果不需要使用這種方法但是又想讓其他對象不要訪問內部數據,那么可以使用單下劃線。這不過是個習慣,但的確有實際效果。例如,前面有下劃線的名字都不會被帶星號的import語句(`from module import *`)導入(有些語言支持多層次的成員變量(特性)私有性。比如Java就支持4種級別。盡管單下劃線在某種程度上給出兩個級別的私有性,但Python并沒有真正的私有化支持)。
### 7.2.4 類的命名空間
下面的兩個語句(幾乎)等價:
```
def foo(x):
return x * x
foo = lambda x: x * x
```
兩者都創建了返回參數平方的函數,而且都將變量`foo`綁定到函數上。變量`foo`可以在全局(模塊)范圍進行定義,也可處于局部的函數或方法內。定義類時,同樣的事情也會發生,所有位于`class`語句中的代碼都在特殊的命名空間中執行——類命名空間(`class namespace`)。這個命名空間可由類內所有成員訪問。并不是所有Python程序員都知道類的定義其實就是執行代碼塊,這一點非常有用,比如,在類的定義區并不只限定只能使用self語句:
```
>>> class C:
... print "Class C being defined..."
...
Class C being defined...
>>>
```
看起來有點傻,但是看看下面的:
```
>>> class MemberCounter:
... members = 0
... def init(self):
... MemberCounter.members += 1
...
>>> m1 = MemberCounter()
>>> m1.init()
>>> MemberCounter.members
1
>>> m2 = MemberCounter()
>>> m2.init()
>>> MemberCounter.members
2
```
上面的代碼中,在類作用域內定義了一個可供所有成員(實例)訪問的變量,用來計算類的成員數量。注意`init`用來初始化所有實例:第九章中,我會讓這一過程自動化(即把它變成一個適當的構造函數)。
就像方法一樣,類作用域內的變量也可以被所有實例訪問:
```
>>> m1.members 2
>>> m2.members 2
```
那么在實例中重綁定`members`特性呢?
```
>>> m1.members = "Two"
>>> m1.members 'Two'
>>> m2.members
2
```
新`members`值被寫到了`m1`的特性中,屏蔽了類范圍內的變量。這跟函數內的局部和全局變量的行為十分類似,就像第六章討論的”屏蔽的問題“。
### 7.2.5 指定超類
就像本章前面我們討論的一樣,子類可以擴展超類的定義。將其他類名寫在`class`語句后的圓括號內可以指定超類:
```
class Filter():
def init(self):
self.blocked = []
def filter(self, sequence):
return [x for x in sequence if x not in self.blocked]
class SPAMFilter(Filter):
# SPAMFilter是Filter的子類
def init(self):
# 重寫Filter超類中的init方法
self.blocked = ["SPAM"]
```
`Filter`是個用于過濾序列的通用類,事實上它不能過濾任何東西:
```
>>> f = Filter()
>>> f.init()
>>> f.filter([1, 2 ,3])
[1, 2, 3]
```
`Filter`類的用處在于它可以用作其他類的基類(超類),比如`SPAMFilter`類,可以將序列中的`'SPAM'`過濾出去。
```
>>> s = SPAMFilter()
>>> s.init()
>>> s.filter(["SPAM", "SPAM", "SPAM", "SPAM", "eggs", "bacon", "SPAM"])
['eggs', 'bacon']
```
注意`SPAMFilter`定義的兩個要點。
? 這里用提供新定義的方式重寫了`Filter`的`init`定義。
? `filter`方法的定義是從`Filter`類中拿過來(繼承)的,所以不用重寫它的定義。
第二個要點揭示了繼承的用處:我可以寫一大堆不同的過濾類,全部都從`Filter`繼承,每一個我都可以使用已經實現的`filter`方法。這就是前面提到過的有用的懶惰。
### 7.2.6 檢查繼承
如果想要查看一個類是否是另一個的子類,可以使用內建的`issubclass`函數:
```
>>> issubclass(SPAMFilter, Filter)
True
>>> issubclass(Filter, SPAMFilter)
False
```
如果想要知道已知的基類(們),可以直接使用它的特殊特性`__bases__`。
```
>>> SPAMFilter.__bases__ (<class __main__.Filter at 0x7fa160e4a4c8>,)
>>> Filter.__bases__ ()
```
同樣,還能用使用`isinstance`方法檢查一個對象是否是一個類的實例:
```
>>> s = SPAMFilter()
>>> isinstance(s, SPAMFilter)
True
>>> isinstance(s, Filter)
True
>>> isinstance(s, str)
False
```
_注:使用`isinstance`并不是個好習慣,使用多態會更好一些。_
可以看到,s是`SPAMFilter`類的(直接)實例,但是它也是`Filter`類的間接實例,因為`SPAMFilter`是`Filter`的子類。另外一種說法就是`SPAMFilter`類就是`Filters`類。可以從前一個例子中看到,`isinstance`對于類型也起作用,比如字符串類型(`str`)。
如果只想知道一個對象屬于哪個類,可以使用`__class__`特性:
```
>>> s.__class__
<class __main__.SPAMFilter at 0x7fa160e4a530>
```
_注:如果使用`__metaclass__ = type`或從`object`繼承的方式來定義新式類,那么可以使用`type(s)`查看實例所屬的類。_
### 7.2.7 多個超類
可能有的讀者注意到了上一節中的代碼有些奇怪:也就是`__bases__`這個復數形式。而且文中也提到過可以找到一個新的基類(們),也就按暗示它的基類可能會多余一個。事實上就是這樣,建立幾個新的類來試試看:
```
class Calculator:
def calculate(self, expression):
self.value = eval(expression)
class Talker:
def talk(self):
print "Hi, my value is", self.value
class TalkingCalculator(Calculator, Talker):
pass
```
子類(`TalkingCalculator`)自己不做任何事,它從自己的超類繼承所有的行為。它從`Calculator`類那里繼承`calculate`方法,從`Talker`類那里繼承`talk`方法,這樣它就成了會說話的計算器(talking calculator)。
```
>>> tc = TalkingCalculator()
>>> tc.calculate("1 + 2 * 3")
>>> tc.talk()
Hi, my value is 7
```
這種行為稱為_多重繼承_(multiple inheritance),是個非常有用的工具。但除非讀者特別熟悉多重繼承,否則應該盡量避免使用,因為有些時候會出現不可預見的麻煩。
當使用多重繼承時,有個需要注意的地方。如果一個方法從多個超類繼承(也就是說你有兩個具有相同名字的不同方法),那么必須要注意一下超類的順序(在`class`語句中):先繼承的類中的方法會重寫后繼承的類中的方法。所以如果前例中`Calculator`類也有個叫做`talk`的方法,那么它就會重寫`Talker`的`talk`方法(使其不可訪問)。如果把它們的順序調過來,像下面這樣:
```
class TalkingCalculator(Talker, Calculator):
pass
```
就會讓`Talker`的`talk`方法可用了。如果超類們共享一個超類,那么在查找給定方法或者屬性時訪問超的順序稱為MRO(Method Resolution Order, 方法判定順序),使用的算法相當復雜。幸好,它工作得很好,所以不用過多關心。
### 7.2.8 接口與內省
“接口”的概念與多態有關。在處理多態對象時,只要關心它的接口(或稱“協議”)即可,也就是公開的方法和特性。在Python中,不用顯式地指定對象必須包含哪些方法才能作為參數接收。例如,不用(像在Java中一樣)顯式地編寫接口,可以在使用對象的時候假定它可以實現你所要求的行為。如果它不能實現的話,程序就會失敗。
一般來說只需要讓對象符合當前的接口(換句話說就是實現當前方法),但是還可以更靈活一些。除了調用方法然后期待一切順利之外,還可檢查所需方法是否已經存在。如果不存在,就需要做些其他事情:
```
>>> hasattr(tc, "talk")
True
>>> hasattr(tc, "fnord")
False
```
_注:`callable`函數在Python3.0中已不再可用。可以使用`hasattr(x, "__call__")`來代替`callable(x)`。_
這段代碼使用了`getattr`函數,而沒有在`if`語句內使用`hasattr`函數直接訪問特性,`getattr`函數允許提供默認值(本例中為`None`),以便在特性不存在時使用,然后對返回的對象使用`callable`函數。
_注:與`getattr`相對應的函數是`setattr`,可以用來設置對象的特性:_
```
>>> setattr(tc, "name", "Mr. XuHoo")
>>> tc.name 'Mr. XuHoo'
```
如果要查看對象內所有存儲的值,那么可以使用`__dict__`特性。如果真的想要找到對象是由什么組成的,可以看看`inspect`模塊。這是為那些想要編寫對象瀏覽器(以圖形方式瀏覽Python對象的程序)以及其他需要類似功能的程序的高級用戶準備的。關于對象和模塊的更多信息,可以參見10.2節。
## 7.3 一些關于面向對象設計的思考
關于面向對象設計的書籍已經有很多,盡管這并不是本書所關注的主題,但是還是給出一些要點。
? 將屬于一類的對象放在一起。如果一個函數操縱一個全局變量,那么兩者最好都在類內作為特性和方法出現。
? 不要讓對象過于親密。方法應該只關心自己實例的特性。讓其他實例管理自己的狀態。
? 要小心繼承,尤其是多重繼承。繼承機制有時很有用,但也會在某些情況下讓事情變得過于復雜。多繼承難以正確使用,更加難以調試。
? 簡單就好。讓你的方法小巧。一般來說,多數方法都應能在30秒內被讀完(以及理解),盡量將代碼行數控制在一頁或者一屏之內。
當考慮需要什么類以及類要有什么方法時,應該嘗試下面的方法。
(1)寫下問題的描述(程序要做什么),把所有的名詞、動詞和形容詞加下劃線。
(2)對于所有名詞,用作可能的類。
(3)對于所有動詞,用作可能的方法。
(4)對于所有形容詞,用作可能的特性。
(5)把所有方法和特性分配到類。
現在已經有了_面向對象模型_的草圖了。還可以考慮類和對象之間的關系(比如繼承或協作)以及它們的作用,可以用以下步驟精煉模型。
(1)寫下(或者想象)一系列的_使用實例_,也就是程序應用時的場景,試著包括所有的功能。
(2)一步步考慮每個使用實例,保證模型包括所有需要的東西。如果有些遺漏的話就添加進來。如果某處不太正確則改正。繼續,直到滿意為止,
當認為已經有了可以應用的模型時,那就可以開工了。可能需要修正自己的模型,或者是程序的一部分。幸好,在Python中不用過多關心這方面的事情,因為很簡單,只要投入進去就行(如果需要面向對象程序設計方面的更多指導,請參見第十九章推薦的書目)。
## 7.4 小結
本章不僅介紹了更多關于Python語言的信息,并且介紹了幾個可能完全陌生的概念。下面總結一下。
? 對象:對象包括特性和方法。特性只是作為對象的一部分變量,方法則是存儲在對象內的函數。(綁定)方法和其他函數的區別在于方法總是將對象作為自己的第一個參數,這個參數一般稱為self。
? 類:類代表對象的集合(或一類對象),每個對象(實例)都有一個類。類的主要任務是定義它的實例會用到的方法。
? 多態:多態是實現將不同類型和類的對象進行同樣對待的特性——不需要知道對象屬于哪個類就能調用方法。
? 封裝:對象可以將它們的內部狀態隱藏(或封裝)起來。在一些語言中,這意味著對象的狀態(特性)只對自己的方法可用。在Python中,所有的特性都是公開可用的,但是程序員應該在直接訪問對象狀態時謹慎行事,因為他們可能無意中使得這些特性在某些方面不一致。
? 繼承:一個類可以是一個或者多個類的子類。子類從超類繼承所有方法。可以使用多個超類,這個特性可以用來組成功能的正交部分(沒有任何聯系)。普通的實現方式是使用核心的超類和一個或者多個混合的超類。
? 接口和內省:一般來說,對于對象不用探討過深。程序員可以靠多態調用自己需要的方法。不過如果想要知道對象到底有什么方法和特性,有些函數可以幫助完成這項工作。
? 面向對象設計:關于如何(或者說是否應該進行)面向對象設計有很多的觀點。不管你持什么觀點,完全理解這個問題,并且創建容易理解的設計是很重要的。
### 7.4.1 本章的新函數
本章涉及的新函數如表7-1所示。
**表7-1 本章的新函數**
```
callable(object) 確定對象是否可調用(比如函數或者方法)
getattr(object, name[ ,default]) 確定特性的值,可選擇提供默認值
hasattr(object, name) 確定對象是否有給定的特性
isinstance(object, class) 確定對象是否是類的實例
issubclass(A, B) 確定A是否為B的子類
random.choice(sequence) 從非空序列中隨機選擇元素
setattr(object, name, value) 設定對象的給定特性為value
type(object) 返回對象的類型
```
### 7.4.2 接下來學什么
前面已經介紹了許多關于創建自己的對象以及自定義對象的作用。在輕率地進軍Python特殊方法的魔法陣(第九章)之前,讓我們先喘口氣,看看介紹異常處理的簡短的一章。