[TOC]
在編程世界中,重復代碼被認為是邪惡的。我們不應該讓相同或相似代碼的多個副本存在不同的地方。
</b>
有許多方法可以用來合并具有類似功能的代碼或對象。在本章中,我們將講述最著名的面向對象原則:繼承。如第1章“面向對象設計”所述,繼承允許我們創建兩個或多個類之間的*是一個*關系,將公共邏輯抽象轉換成超類,在子類中管理特定細節。我們將介紹以下方面的Python語法和原則:
* 基本繼承
* 從內置類繼承
* 多重繼承
* 多態性和鴨子類型
## 基本繼承
技術上,我們創建的每個類都使用了繼承。所有的Python類都是名為`object`的特殊類的子類。這個類提供非常少的數據和行為(它提供的行為都是雙下劃線方法,僅供內部使用。譯注:使用`dir(object)`查看,感覺方法還是很多的!),但它確實允許Python以相同的方式處理所有對象。
</b>
如果我們沒有從不同的類顯式繼承,我們的類將自動從`object`繼承。然而,我們可以使用以下語法公開聲明我們的類來自`object`:
```
class MySubClass(object):
pass
```
這就是繼承!從技術上講,這個例子和我們第2章“Python中的對象”中的第一個示例沒有什么不同,因為,如果我們不顯式提供一個不同的超類,Python 3將自動繼承自`object`。超類或父類是被繼承的類。子類是繼承自超類的類。在這種例子中,超類是`object`,而`MySubClass`是子類。子類也可以說是從它的父類派生出來的,或者說子類擴展了父類。
</b>
正如你可能已經從示例中想到的,繼承在基本類定義之上需要一點最少的額外語法量。只需在類名后冒號(用于終止類定義)前的括號內包含父類的名字即可。我們唯一要做的事,就是告訴Python新類是從給定的超類派生出來的。
</b>
我們如何在實踐中應用繼承?最簡單和最明顯的繼承是向現有類添加功能。讓我們從一個簡單的跟蹤幾個人的姓名和電子郵件地址的聯系人管理器開始。這個`contact`類在一個類變量中維護所有聯系人的列表,以及初始化每個聯系人的姓名和地址:
```
class Contact:
all_contacts = []
def __init__(self, name, email):
self.name = name
self.email = email
Contact.all_contacts.append(self)
```
這個例子向我們介紹了類變量。所有聯系人`all_contacts`列表,因為它是類定義的一部分,會被該類的所有實例共享。這意味著只有一個`Contact.all_contacts`列表,我們可以訪問`Contact.all_contacts`。不太明顯的是,我們也可以從`Contact`實例化的對象上通過`self.all_contacts`訪問這個列表。如果在對象上找不到這個列表,可以在類中找到,所以,無論是類或是類的對象,都指向同一個列表。
> 小心使用這個語法,因為如果你曾經使用`self.all_contacts`,你實際上將創建一個僅與該對象相關聯的新實例變量。類變量仍然會保持不變,仍可通過`Contact.all_contacts`訪問。
(譯注:貌似和上面說的不太一樣,如下圖所示,使用`self.all_contacts`后,類變量`Contact.all_contacts`沒有保持不變)


這是一個簡單的類,它允許我們跟蹤每個聯系人的數據。但是如果我們的一些聯系人也是我們需要從他們那里采購貨物的供應商呢?我們可以向`Contact`類添加一個`order`方法,但這將允許人們意外地從顧客或家庭朋友們等聯系人那里訂購東西。相反,讓我們創建一個新的供應商類,其行為類似于我們的聯系人類,但有一個額外的訂購`order`方法:
```
class Supplier(Contact):
def order(self, order):
print("If this were a real system we would send "
"'{}' order to '{}'".format(order, self.name))
```
現在,如果我們在我們信任的解釋器中測試這個類,我們會看到所有的聯系人、供應商在其`__init__`中都接受一個姓名和電子郵件地址,但僅限于供應商有一個可應用的訂購`order`方法:
```
>>> c = Contact("Some Body", "somebody@example.net")
>>> s = Supplier("Sup Plier", "supplier@example.net")
>>> print(c.name, c.email, s.name, s.email)
Some Body somebody@example.net Sup Plier supplier@example.net
>>> c.all_contacts
[<__main__.Contact object at 0xb7375ecc>,
<__main__.Supplier object at 0xb7375f8c>]
>>> c.order("I need pliers")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Contact' object has no attribute 'order'
>>> s.order("I need pliers")
If this were a real system we would send 'I need pliers' order to
'Sup Plier '
```
因此,現在我們的供應商`Supplier`類可以做任何聯系人`Contact`類可以做的事情(包括把它自己添加到所有聯系人`all_contacts`列表)和它作為供應商需要處理的獨特的事情。這就是繼承的美。
### 擴展內置類
繼承的一個有趣的用途是給內置類添加功能。在前面看到的聯系人`Contact`類中,我們將聯系人添加到所有聯系人的列表中。如果我們想按名字搜索這個列表呢?嗯,我們可以在`Contact`類上加上一個搜索方法,但實際上這個方法更像是屬于列表(一種內置數據結構)本身。我們可以通過繼承來做到這一點:
```
class ContactList(list):
def search(self, name):
'''返回名字中包含搜索值的聯系人'''
matching_contacts = []
for contact in self:
if name in contact.name:
matching_contacts.append(contact)
return matching_contacts
class Contact:
all_contacts = ContactList()
def __init__(self, name, email):
self.name = name
self.email = email
self.all_contacts.append(self)
```
我們創建了一個新的`ContactList`類來擴展內置列表`list`,并用`ContactList`類替代`list`來創建類變量。然后,我們實例化這個子類作為我們的所有聯系人`all_contacts`列表。我們可以測試新的搜索功能,如下所示:
```
>>> c1 = Contact("John A", "johna@example.net")
>>> c2 = Contact("John B", "johnb@example.net")
>>> c3 = Contact("Jenna C", "jennac@example.net")
>>> [c.name for c in Contact.all_contacts.search('John')]
['John A', 'John B']
```
(譯注:最后一行可以簡單寫成`[c.name for c in Contact.all_contacts if "John" in c.name]`,這樣就不用寫`search`方法了)
你想知道我們是如何做到把內置語法`[]`變成我們可繼承的形式嗎?用`[]`創建空列表實際上是使用`list()`創建空列表的簡寫;這兩種語法實際是相同的:
```
>>> [] == list()
True
```
事實上,`[]`語法是所謂的**語法糖**,它被稱為鉤子(hood),調用`list()`的構造器。列表`list`數據類型是一個我們可以擴展的類。事實上,列表`list`本身擴展了對象`object`類:
```
>>> isinstance([], object)
True
```
作為第二個例子,我們可以擴展字典`dict`類,它類似于列表,當使用`{}`語法縮寫時,它將被構造(注意取`key`和條件語句):
```
class LongNameDict(dict):
def longest_key(self):
longest = None
for key in self:
if not longest or len(key) > len(longest):
longest = key
return longest
```
這很容易在交互式解釋器中測試:
```
>>> longkeys = LongNameDict()
>>> longkeys['hello'] = 1
>>> longkeys['longest yet'] = 5
>>> longkeys['hello2'] = 'world'
>>> longkeys.longest_key()
'longest yet'
```
大多數內置類型都可以類似地進行擴展。常見的可被擴展的內置類型有對象`object`、列表`list`、集合`set`、字典`dict`、文件`file`和字符串`str`。數值類型,如`int`和`float`偶爾也會被擴展。
### 重寫和Super
因此,繼承對于向現有的類*添加*新的行為是很好的,但是如果是*改變*行為呢?我們的聯系人類只允許姓名和電子郵件地址。這對于大多數聯系人來說可能已經足夠了,但是如果我們想給親密朋友添加一部電話號碼呢?
</b>
正如我們在第2章“Python中的對象”中看到的,在我們構造完一個聯系人對象后,我們可以通過設置添加聯系人的電話屬性。但是如果我們想在初始化時添加這個第三個變量(電話屬性),我們必須重寫`__init__`。重寫意味著在子類中修改或替換和超類同名的方法。這樣做并不需要特殊語法;子類將自動調用新創建的方法,而不是超類的方法。例如:
```
class Friend(Contact):
def __init__(self, name, email, phone):
self.name = name
self.email = email
self.phone = phone
```
任何方法都可以被重寫,而不僅僅是`__init__`。然而,在我們繼續之前,我們需要解決這個例子中的一些問題。我們的聯系人`Contact`類和朋友`Friend`類有用來設置姓名和電子郵件屬性的重復代碼;這使得代碼維護變得很復雜,因為我們必須在兩個或多個地方更新代碼。更令人擔憂的是,我們的朋友`Friend`類忽略了將自己添加我們在聯系人`Contact`類中創建的`all_contacts`列表。
</b>
我們真正需要的是一種可以執行`Contact`類原始`__init__`的方法。這就是`super`函數的作用;它返回的對象是父類的實例,允許我們直接調用父類方法:
```
class Friend(Contact):
def __init__(self, name, email, phone):
super().__init__(name, email)
self.phone = phone
```
這個示例首先使用`super`創建一個父類對象的實例,然后調用這個對象的`__init__`方法,傳入預期的參數。然后再執行自己的初始化,即設置電話屬性。
> 請注意,`super()`語法在python的舊版本中不起作用。像`[]`和`{}`對于列表和詞典的意義一樣,它是一個更復雜結構的簡寫。我們將在我們討論多重繼承時,進一步了解它,但現在只要知道,在Python 2中,你必須調用`super(EmailContact,self).__init__()`(譯注:我懷疑`EmailContact`應該寫成`Friend`)。請特別注意,第一個參數是子類的名稱,而不是像很多人希望的是父類的名稱。此外,請記住類參數在對象參數之前。我總是忘記順序,所以Python 3中的新語法省去了我很多檢查的時間。
所以,可以對任何方法使用`super()`,而不僅僅是`__init__`。這意味著一切
方法都可以通過重寫和調用`super`來修改。可以在方法的任何位置調用`super`;我們不必把`super`放在方法中的第一行。例如,我們可能需要操縱或驗證傳入參數,然后再將它們轉發給超類。
## 多重繼承
多重繼承是一個敏感的話題。原則上,它很簡單:一個子類可以從多個父類繼承,并能訪問父類的功能。實際上,這沒有聽起來那么有用,許多專家程序員建議不要使用它。
> 根據經驗,如果你認為你需要多重繼承,你就是可能是錯的,但是如果你知道你需要它,你可能是對的。(譯注:唔知講咩!)
多重繼承最簡單和最有用的形式叫做`mixin`。`mixin`通常是一個超類,它并不意味著為自己而存在,而是意味著被其他類繼承以提供額外的功能。比如說我們希望在聯系人`Contact`類中添加允許發送電子郵件到聯系人郵箱`self.email`的功能。發送電子郵件是一項常見的任務,我們可能想在許多類上使用這個功能。因此,我們可以編寫一個簡單的`mixin`類來為我們實現發送電子郵件的功能:
```
class MailSender:
def send_mail(self, message):
print("Sending mail to " + self.email)
# 在這里加入 e-mail 邏輯
```
為了簡潔起見,我們這里不包括實際的電子郵件邏輯;如果你感興趣它是如何完成的,請參見Python標準庫中的`smtplib`模塊。
</b>
這個類不做任何特殊的事情(事實上,它勉強起到一個獨立類的作用),但是它確實允許我們使用多重繼承定義一個同時描述聯系人`Contact`和郵件發送者`MailSender`的新類:
```
class EmailableContact(Contact, MailSender):
pass
```
多重繼承的語法看起來像類定義中的參數列表。括號中不僅僅包含一個基類,而是包含兩個(或更多)用逗號隔開的類。我們可以測試這個混合類,看看`mixin`的效果:
```
>>> e = EmailableContact("John Smith", "jsmith@example.net")
>>> Contact.all_contacts
[<__main__.EmailableContact object at 0xb7205fac>]
>>> e.send_mail("Hello, test e-mail here")
Sending mail to jsmith@example.net
```
聯系人`Contact`的初始化函數仍將新聯系人添加到`all_contacts`列表中,并且`mixin`能夠發送郵件給`self.email`,??,一切正常。
</b>
這并不難,你可能想知道關于多重繼承可怕的警告是什么?我們一會兒再討論這個復雜的問題,但是先讓我們考慮一些不使用`mixin`的其他選擇:
* 我們可以使用單一繼承并在子類中添加`send_mail`函數。缺點是,對于需要電子郵件的任何其他類,都得復制這個函數。
* 我們可以創建一個獨立的Python函數來發送電子郵件,當需要發送電子郵件時,傳遞正確的電子郵件地址作為函數參數,然后調用該函數。
* 我們可以探索一些使用組合而不是繼承的方法。例如,電子郵件聯系人`EmailableContact`可以有一個郵件發送者`MailSender`對象,而不是繼承它。
* 我們可以進行猴子補丁`monkey-patch`(我們將在第7章“Python面向對象的快捷方式”中簡要介紹猴子補丁)。當聯系人`Contact`類被創建后,我們用`monkey-patch`給這個類添加`send_mail`方法。我們可以定義一個接受`self`參數的函數,并將其設置為現有類的屬性。
當混合的方法來自不同類時,多重繼承工作正常,但是當我們必須在超類上調用方法時,事情將變得非常混亂。有多個超類,我們怎樣知道應該調用哪一個方法?我們該如何知道調用它們的順序是什么?
</b>
讓我們在朋友`Friend`類上添加一個家庭住址來探索這些問題。我們有一些可用的方法。地址是一些字符串的集合,分別代表街道、城市、國家和聯系人的其他詳細信息。我們可以將這些字符串中的每一個作為參數傳遞到`Friend`類的`__init__`方法。我們也可以將這些字符串存儲在元組或字典中,并作為單個參數輸入到`__init__`中。如果沒有給地址添加方法的需要,這可能是最好的方案。
</b>
另一種選擇是創建一個新的地址`Address`類來保存這些字符串,然后將該類的一個實例傳遞到`Friend`類的`__init__`方法中。這個解決方案的優點是我們可以給數據添加行為(比如,根據地址指路或打印地圖的方法),而不僅僅是靜態存儲數據。這是一個組合的例子,正如我們在第1章“面向對象設計”中討論的那樣。“有一個”的組合關系是這個問題的一個完全可行的解決方案。它允許我們在其他實體中重用地址`Address`類,例如建筑物、商業,或者組織。
</b>
然而,繼承也是一個可行的解決方案,這也是我們想要探索的。讓我們添加一個包含地址的新類。我們將這個新類稱為“地址持有者”`AddressHolder`,而不是“地址”,因為繼承定義了一個“是一個”的關系。說“朋友”是“地址”,是不正確的,但因為朋友可以有“地址”,我們就可以稱“朋友”是“地址持有者”。稍后,我們可以創建持有地址的其他實體(公司,建筑)。這是我們的地址持有者`AddressHolder`類:
```
class AddressHolder:
def __init__(self, street, city, state, code):
self.street = street
self.city = city
self.state = state
self.code = code
```
非常簡單,我們只需要獲取所有的數據,并把它們扔進實例變量中進行初始化。
## 鉆石問題
我們可以使用多重繼承來添加這個新類作為現有`Friend`類的父類。棘手的是我們現在有兩個父類`__init__`方法,兩者都需要被初始化。它們使用不同的參數。我們該怎么做?我們可以從一個天真的方法開始:
```
class Friend(Contact, AddressHolder):
def __init__(self, name, email, phone,street, city, state, code):
Contact.__init__(self, name, email)
AddressHolder.__init__(self, street, city, state, code)
self.phone = phone
```
在這個例子中,我們直接在每個超類上調用`__init__`函數,并顯式傳遞`self`參數。這個例子在技術上是可行的;我們可以直接在類中訪問不同的變量。但是有幾個問題。
</b>
首先,如果我們忽略顯式調用超類的初始化函數,則有可能未初始化某個超類。在這個例子里這不是大問題,但它可能導致在常見情況下出現難以調試的問題,并導致程序崩潰。想象一下試圖將數據插入到還沒有連接的數據庫中。
</b>
第二,也是更險惡的,由于類層級的緣故,超類可能會被多次調用。請看這個繼承圖:

朋友`Friend`類中的`__init__`方法首先調用聯系人`Contact`的`__init__`,它隱式初始化`object`超類(記住,所有類都從`object`繼承而來)。然后`Friend`調用`AddressHolder`上的`__init__`,再一次進行隱式初始化`object`超類。這意味著`object`被創建了兩次。對于`object`類,這不是什么大問題,但是在某些情況下,它可能產生會災難。想象一下,每個請求都要連接數據庫兩次!
</b>
基類應該只能調用一次。一次,是的,那么什么時候調用呢?我們是先調用`Friend`,然后`Contact`,然后`object`,然后`AddressHolder`?還是先調用`Friend`,然后`Contact`,然后`AddressHolder`,然后是`object`?
> 調用方法的順序可以通過修改上類的`__mro__`(方法解析順序)屬性來實現。這超出了本書的范圍。如果你需要理解它,我推薦你閱讀Expert Python Programming, Tarek Ziadé, Packt Publishing,,或閱讀關于該主題的原始文檔,網址為[http://www.python.org/download/releases/2.3/mro/](http://www.python.org/download/releases/2.3/mro/).
讓我們看第二個人為的例子,它更清楚地說明了這個問題。這里我們有一個基類,它有一個名為`call_me`的方法。兩個子類重寫該方法,然后另一個子類多重繼承了這兩個子類。這被稱為鉆石繼承,因為類圖像鉆石的形狀:

讓我們把這張圖轉換成代碼;此示例顯示了調用方法的時間:
```
class BaseClass:
num_base_calls = 0
def call_me(self):
print("Calling method on Base Class")
self.num_base_calls += 1
class LeftSubclass(BaseClass):
num_left_calls = 0
def call_me(self):
BaseClass.call_me(self)
print("Calling method on Left Subclass")
self.num_left_calls += 1
class RightSubclass(BaseClass):
num_right_calls = 0
def call_me(self):
BaseClass.call_me(self)
print("Calling method on Right Subclass")
self.num_right_calls += 1
class Subclass(LeftSubclass, RightSubclass):
num_sub_calls = 0
def call_me(self):
LeftSubclass.call_me(self)
RightSubclass.call_me(self)
print("Calling method on Subclass")
self.num_sub_calls += 1
```
此示例簡單地確保每個重寫的`call_me`方法直接調用同名的父類方法。每次調用方法時,它都會將信息打印到屏幕上,讓我們知道有一個方法被調用。類中有一個隨時更新的靜態變量,用于顯示它被調用了多少次。如果我們實例化一個`Subclass`類對象,并調用它的`call_me`方法一次,我們將得到這樣的輸出:
```
>>> s = Subclass()
>>> s.call_me()
Calling method on Base Class
Calling method on Left Subclass
Calling method on Base Class
Calling method on Right Subclass
Calling method on Subclass
>>> print(
... s.num_sub_calls,
... s.num_left_calls,
... s.num_right_calls,
... s.num_base_calls)
1 1 1 2
```
因此,我們可以清楚地看到基類的`call_me`方法被調用了兩次。如果該方法真的用在實際工作中,可能會導致一些潛在的錯誤——比如存入銀行賬戶——兩次。
</b>
需要記住的一點是,在多重繼承中,我們只想調用類層次結構中的“下一個”方法,而不是“父類”方法。事實上,下一個方法可能不在當前類的父類或祖先類上。`super`關鍵詞再次拯救我們。實際上,`super`最初就是為了使多重繼承的復雜形式成為可能。這里使用`super`寫的相同的代碼:
```
class BaseClass:
num_base_calls = 0
def call_me(self):
print("Calling method on Base Class")
self.num_base_calls += 1
class LeftSubclass(BaseClass):
num_left_calls = 0
def call_me(self):
super().call_me()
print("Calling method on Left Subclass")
self.num_left_calls += 1
class RightSubclass(BaseClass):
num_right_calls = 0
def call_me(self):
super().call_me()
print("Calling method on Right Subclass")
self.num_right_calls += 1
class Subclass(LeftSubclass, RightSubclass):
num_sub_calls = 0
def call_me(self):
super().call_me()
print("Calling method on Subclass")
self.num_sub_calls += 1
```
變化很小;我們簡單地用super()代替了直接調用,底部子類只調用`super()`一次,而不必調用左右兩邊的子類方法。變化很簡單,但是讓我們看看執行后的不同之處:
```
>>> s = Subclass()
>>> s.call_me()
Calling method on Base Class
Calling method on Right Subclass
Calling method on Left Subclass
Calling method on Subclass
>>> print(s.num_sub_calls, s.num_left_calls, s.num_right_calls,s.num_base_calls)
1 1 1 1
```
看起來不錯,我們的基類方法只被調用一次。但是`super()`在這里到底做了些什么?因為打印`print`語句是在`super()`調用之后執行的,所以打印的輸出的順序是每個方法實際執行的順序。讓我們從后往前看看誰在調用什么。
</b>
首先,`Subclass`的`call_me`調用`super().call_me()`,指的是`LeftSubclass.call_me()`。`LeftSubclass.call_me()`方法調用
`super().call_me()`,但在這種情況下,`super()`指的是`RightSubclass.call_ me()`。
</b>
請特別注意:`super`調用的不是左子類`LeftSubclass`的超類(`BaseClass`)。相反,它調用的是右子類`RightSubclass`,即使它不是左子類的直接父類!這是`下一個`方法,不是父類方法。右子類然后調用基類,`super`調用能確保類層次結構中的每個方法只被執行一次。
### 不同的參數集
當我們回到我們的`Friend`多重繼承示例時,會發現事情變得越來復雜。在`Friend`的`__init__`方法中,我們最初不得不*使用不同的參數集*,調用兩個父類的`__init__`:
```
Contact.__init__(self, name, email)
AddressHolder.__init__(self, street, city, state, code)
```
當我們使用`super`時,我們是如何管理不同的參數集呢?我們不必知道`super`將首先嘗試初始化哪個類。即使我們這樣做了,我們仍然需要一種傳遞“額外”參數的方法,以便于后續在其他子類上對`super`的調用,能夠接收正確的參數。
</b>
特別是,如果對`super`的第一次調用,傳遞了`name`和`email`參數給`Contact.__init__`。`Contact.__init__`繼續調用`super`,它需要將與地址相關的參數傳遞給“下一個”方法,也就是`AddressHolder.__init__`。
</b>
每當我們調用有相同名字、但有不同的參數集的超類方法時,這會產生一個問題。大多數時候,你唯一需要調用一個完整的、具有不同的參數集的超類方法,是在`__init__`進行的,正如我們在這里所做的。然而,對于常規方法,我們可能希望添加僅對一個子類或一組子類有意義的可選參數。
</b>
可悲的是,解決這個問題的唯一方法是從開始就做好計劃。我們必須設計我們的基類參數列表來接受任何并非每個子類實現都需要的參數。最后,我們必須確保方法能夠自由地接受并不期望的參數,并將它們傳遞給`super`調用,這對繼承順序中的后續方法是必要的。
</b>
Python函數的參數語法提供了實現這一點所需的所有工具,但是它使得整個代碼看起來很麻煩。我們看看正確版本的`Friend`多重繼承代碼:
```
class Contact:
all_contacts = []
def __init__(self, name='', email='', **kwargs):
super().__init__(**kwargs)
self.name = name
self.email = email
self.all_contacts.append(self)
class AddressHolder:
def __init__(self, street='', city='', state='', code='',**kwargs):
super().__init__(**kwargs)
self.street = street
self.city = city
self.state = state
self.code = code
class Friend(Contact, AddressHolder):
def __init__(self, phone='', **kwargs):
super().__init__(**kwargs)
self.phone = phone
```
我們將所有參數改為關鍵字參數,并給它們一個空值字符串作為默認值。我們還應確保`**kwargs`參數也包含在方法中,代表方法中尚不知道的任何附加參數。它通過`super`調用將這些參數傳遞給下一個類。(譯注:如果我把`Contact`類的`super`給注釋掉,`Friend`類的實例對象就不會有地址信息了)
> 如果您不熟悉`**kwargs`語法,就當它是在參數列表中不能顯示列出的任何關鍵字參數。這些參數存儲在名為`kwargs`的字典里(我們可以隨意給這個字典命名,但一般建議使用`kw`或`kwargs`)。當我們調用一個不同的帶有`**kwargs`語法的方法(例如,`super().__init__`),它會打開字典并將字典中的內容作為普通的關鍵字參數傳遞給方法。我們將在第7章“Python面向對象快捷方式”中詳細討論這一點。
前面的例子做了它應該做的事情。但它開始看起來亂七八糟了,很難回答這樣一個問題:我們需要哪些參數傳遞給`Friend.__init__`?這對任何計劃使用這個類的人來說都是至關重要的,所以應該向方法中添加一個`docstring`來解釋發生了什么。
</b>
此外,如果我們想*重用*父類中的變量,我們所做的事情是不夠的。當我們將`**kwargs`變量傳遞給`super`時,字典不會包括作為顯式關鍵字參數的任何變量。例如,在`Friend.__init__`,對`super`的調用中,并沒有將`phone`包含在`kwargs`字典中。如果任何其他類需要`phone`參數,我們需要確保它在傳遞的字典中。更糟糕的是,如果我們忘記這樣做,它會很難調試,因為超類不會抱怨,只是簡單地分配一個變量的默認值(在本例中為空字符串)。
</b>
有幾種方法可以確保變量向上傳遞。假設由于某種原因,`Contact`類確實需要`phone`參數初始化,`Friend`類也需要訪問`phone`參數。我們可以執行以下任一操作:
* 不要將`phone`作為顯式關鍵字參數。相反,把它放在`kwargs`字典里。`Friend`可以使用`kwargs['phone']`語法查找它。當它將`**kwargs`傳遞給`super`調用時,`phone`參數仍然會在字典里。
* 將`phone`參數作為顯式關鍵字參數,但在傳遞給`super`之前,使用標準字典語法`kwargs['phone'] = phone`更新`kwargs`字典。
* 將`phone`參數作為顯式關鍵字參數,但使用`kwargs.update`方法更新字典。如果有幾個參數要更新,這會很有用。你可以使用`dict(phone=phone)`構造函數或字典語法`{"phone":phone }`來創建一個,并對傳入的詞典進行更新`update`。
* 將`phone`參數作為顯示關鍵詞參數,但使用語法`super().__init__(phone=phone, **kwargs)`,將其傳遞給`super`調用。
我們已經討論了Python中涉及多重繼承的許多警告。當我們需要考慮所有可能的情況時,我們必須為它們做好計劃,我們的代碼會變得混亂。基本的多重繼承可能很方便,但是,在許多情況下在這種情況下,我們可能希望選擇一種更透明的方式來合并兩個完全不同的類,通常使用組合或者我們將要在第10章“Python設計模式I”和第11章“Python設計模式II”中討論的某種設計模式。
## 多態性
我們在第1章“面向對象設計”中被介紹了多態性。這是描述一個簡單概念的奇特名字:不同行為的發生取決于正在使用哪個子類,而不必明確知道這個子類是什么。例如,想象一個播放音頻文件的程序。媒體播放器可能需要加載音頻文件`AudioFile`對象,然后播放`play`它。我們會這個對象上調用`play()`方法。這個`play()`方法負責解壓縮或提取對象上的音頻,并將它發送到聲卡和揚聲器。播放音頻文件`AudioFile`的行為可以很簡單:
```
audio_file.play()
```
然而,對于不同類型的音頻文件,解壓縮和提取音頻文件的過程非常不同的。`.wav`文件存儲未壓縮的音頻,而`. mp3`、`.wma`,還有`.ogg`文件都有著完全不同的壓縮算法。
</b>
我們可以使用多態性繼承來簡化設計。每種類型文件可以由音頻文件的不同子類來表示,例如,`WavFile`,`MP3`文件。其中每一個都有一個`play()`方法,但是這個方法應該對于每個文件有不同地實現,以確保緊隨其后的正確的提取過程。媒體播放器對象永遠不需要知道音頻文件`AudioFile`所指的子類是什么;它只是調用`play()`并多形態地讓對象自己注意播放的實際細節。讓我們來看一個程序:
```
class AudioFile:
def __init__(self, filename):
if not filename.endswith(self.ext):
raise Exception("Invalid file format")
self.filename = filename
class MP3File(AudioFile):
ext = "mp3"
def play(self):
print("playing {} as mp3".format(self.filename))
class WavFile(AudioFile):
ext = "wav"
def play(self):
print("playing {} as wav".format(self.filename))
class OggFile(AudioFile):
ext = "ogg"
def play(self):
print("playing {} as ogg".format(self.filename))
```
所有音頻文件都要經過檢查,以確保初始化時給出了有效的擴展名。但是你注意到父類中的`__init__`方法是如何訪問不同子類的`ext`類變量的嗎?這就是多態性。如果文件名稱沒有以正確的名稱結尾,就會引發異常(異常將在下一章中詳細介紹)。實際上,音頻文件`AudioFile`沒有存儲對`ext`變量的引用,并不能阻止它在子類中訪問`ext`變量。
</b>
此外,音頻文件的每個子類以不同的方式實現`play()`(這個例子實際上并不播放音樂;音頻壓縮算法真的值得寫一本單獨的書!)。這也是多態性在起作用。媒體播放器可以使用完全相同的代碼來播放文件,不管它是什么類型;查看它正在播放的音頻文件是哪個子類,并不重要。解壓縮音頻文件的細節被*封裝*了。如果我們測試這個例子,它會像我們希望的那樣工作:
```
>>> ogg = OggFile("myfile.ogg")
>>> ogg.play()
playing myfile.ogg as ogg
>>> mp3 = MP3File("myfile.mp3")
>>> mp3.play()
playing myfile.mp3 as mp3
>>> not_an_mp3 = MP3File("myfile.ogg")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "polymorphic_audio.py", line 4, in __init__
raise Exception("Invalid file format")
Exception: Invalid file format
```
看出`AudioFile.__init__`能夠檢查文件類型,而無需實際知道它指的是哪個子類嗎?(譯注:子類類變量對父類是可見的)
</b>
多態性實際上是面向對象編程最酷的事情之一,這使得一些早期范例不可能的編程設計變得顯而易見。然而,由于鴨子類型,Python使得多態變得不那么酷(譯注:多態和鴨子類型是不同的)。Python中的鴨子類型允許我們使用任何提供所需行為的對象而不必強迫它成為子類。python的動態特性使得這一點變得微不足道。以下示例沒有擴展音頻文件`AudioFile`,但可以在python中使用完全相同的接口進行交互:
```
class FlacFile:
def __init__(self, filename):
if not filename.endswith(".flac"):
raise Exception("Invalid file format")
self.filename = filename
def play(self):
print("playing {} as flac".format(self.filename))
```
我們的媒體播放器可以像擴展音頻文件`AudioFile`一樣輕松地播放這個對象。
</b>
使用多態性的最重要原因之一是在面向對象的上下文之間使用繼承。因為任何提供正確接口的對象都可以在Python中互換使用,它減少了對多態公共超類的需求。繼承對于共享代碼仍然是有用的,但是,如果共享的都是公共接口,那么只需要鴨子類型。這種對繼承需求的減少也減少了多重繼承的需求;通常,當多重繼承似乎是一個有效的解決方案,我們就可以用鴨子類型來模仿多個超類中的一個。
</b>
當然,僅僅因為一個對象滿足特定的接口(通過提供所需的方法或屬性)并不意味著它在所有情況下都能簡單地工作。必須以在整個系統中有意義的方式實現接口。僅僅因為一個對象提供`play()`方法并不意味著它將自動與媒體播放器一同工作。例如,我們在第1章“面向對象設計”中,也有一個象棋AI對象,也有一個`play()`方法用來移動棋子。即使它滿足了接口,如果我們嘗試將它插入媒體播放器,這個類仍然會以某種特別的方式崩潰!
</b>
鴨子類型的另一個有用特性是鴨子類型的對象只需要提供那些實際被訪問的方法和屬性。例如,如果我們需要創建一個假的文件對象來讀取里面的數據,我們可以創建一個新的具有`read()`方法的對象;如果與對象交互的代碼只是從文件中讀取,那我們不必重寫`write`方法。簡而言之,鴨子類型不需要提供一個可用對象的完整的接口,它只需要實現實際被訪問的接口的就行。
## 抽象基類
雖然鴨子類型很有用,但要提前判定一個類是否滿足你的要求,是不容易的。因此,Python引入了抽象基類。抽象基類或ABCs定義了一個類必須擁有的一組方法和屬性,只有這樣,它才能被認為是鴨子類型的類實例。抽象基類可以擴展抽象基類本身,以便用作實例,但它必須提供所有適當的方法。
</b>
實際上,很少需要創建新的抽象基類,但是我們可能會發現實現現有ABCs實例的場合。我們先介紹ABCs的實現,然后在簡要地看看如何創建自己的抽象基類(如果需要的話)。
### 使用抽象基類
Python標準庫中存在的大多數抽象基類都位于`collections`模塊中。最簡單的一個是容器`Container`類。讓我們在Python解釋器中檢查一下這個類需要什么方法:
```
>>> from collections import Container
>>> Container.__abstractmethods__
frozenset(['__contains__'])
```
因此,容器`Container`類正好有一個抽象方法需要被實現,`__contains__`。你可以通過`help(Container.__contains__)`查看函數簽名是什么樣子的:
```
Help on method __contains__ in module _abcoll:
__contains__(self, x) unbound _abcoll.Container method
```
因此,我們看到`__contains__`需要一個單獨的參數。不幸的是幫助文件并沒有告訴我們這個參數應該是什么的信息,但是從抽象類的名稱來看,這是很明顯的,這個參數是用來檢查容器是否包含的那個值。
</b>
這個方法可以用在列表`list`、字符串`str`和字典`dict`,用來檢查給定值是否在數據結構中。然而,我們也可以定義一個愚蠢的容器,告訴我們一個給定值是否在奇數集合中:
```
class OddContainer:
def __contains__(self, x):
if not isinstance(x, int) or not x % 2:
return False
return True
```
現在,我們可以實例化一個`OddContainer`對象,即使我們沒有通過擴展`Container`類得到`OddContainer`類,`OddContainer`類仍然是容器`Container`對象:
```
>>> from collections import Container
>>> odd_container = OddContainer()
>>> isinstance(odd_container, Container)
True
>>> issubclass(OddContainer, Container)
True
```
這就是為什么鴨子類型比經典多態性更令人敬畏的原因。我們可以創建一個沒有使用繼承(或者更糟糕的是,多重繼承)開銷的關系。(譯注:但是如果給一個新類賦予`list`的方法,鴨子類型似乎并不成立,例如下圖)

容器`Container`抽象類的有趣之處在于,任何實現`Container`抽象類的類都可以自由地使用`in`關鍵字。事實上,`in`只是代表`__contains__`方法的語法糖。任何具有`__contains__`方法的類都是容器`Container`,因此可以通過`in`關鍵字進行查詢,例如:
```
>>> 1 in odd_container
True
>>> 2 in odd_container
False
>>> 3 in odd_container
True
>>> "a string" in odd_container
False
```
### 創建抽象基類
正如我們前面看到的,沒有必要創建抽象基類來啟用鴨子類型。然而,想象我們正在創建一個帶有第三方插件的媒體播放器。在這種情況下,建議創建一個抽象基類來記錄第三方插件應該提供什么樣的API。abc模塊提供了完成此任務所需的工具,但是我要提前警告你,這涉及一些python最神秘的概念:
```
import abc
class MediaLoader(metaclass=abc.ABCMeta):
@abc.abstractmethod
def play(self):
pass
@abc.abstractproperty
def ext(self):
pass
@classmethod
def __subclasshook__(cls, C):
if cls is MediaLoader:
attrs = set(dir(C))
if set(cls.__abstractmethods__) <= attrs:
return True
return NotImplemented
```
這是一個復雜的例子,它包含了幾個在本書的后面才會解釋到的特性。這里只是為了例子的完整,但你沒有必要了解所有這些,也可以創建自己的抽象類。
</b>
第一件奇怪的事情是`metaclass`關鍵字參數被傳遞到類,通常看到的參數是父類列表。這是元類編程的神秘藝術中,一個很少使用的結構。我們不會在本書講述元類,你需要知道的是通過指定`ABCMeta`元類,你的類將具有超能力(或者至少超類能力)。
</b>
接下來,我們看到@abc.abstractmethod和@abc.abstractproperty構造函數。這些是python裝飾器。我們將在第5章“何時使用面向對象編程”中討論。現在,只要知道通過標記一個方法或屬性是抽象的,聲明這個類的任何子類必須實現該方法或提供該屬性,以便被視為類的正式成員。
</b>
看看如果你不給子類提供這些屬性,會發生什么:
```
>>> class Wav(MediaLoader):
... pass
...
>>> x = Wav()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Wav with abstract methods
ext, play
>>> class Ogg(MediaLoader):
... ext = '.ogg'
... def play(self):
... pass
...
>>> o = Ogg()
```
因為`Wav`類無法實現抽象屬性,所以不可能實例化該類。該類仍然是一個合法的抽象類,但是你沒辦法使用子類做任何事情。`Ogg`類提供了這兩個屬性,所以它可以干凈地實例化。
</b>
回到`MediaLoader`抽象類,讓我們剖析`__subclasshook__`方法。它基本上是說,任何提供所有`MediaLoader`抽象屬性的類應被視為`MediaLoader`的一個子類,即使它實際上不是從`MediaLoader`類繼承而來的。
</b>
更常見的面向對象語言在接口和類的實現上,有明顯的不同。例如,一些語言提供顯式的接口`interface`關鍵字,允許我們定義類必須有的方法,但不用實現它們。在這樣的環境中,抽象類既提供了接口,又提供了部分方法(不是所有方法)的具體實現。任何類都可以顯示聲明它實現了一個給定的接口。
</b>
Python的抽象類有助于在不喪失鴨子類型好處的情況下提供接口功能。
### 揭開魔法的神秘面紗
如果你想創建抽象類來滿足這個特殊的契約,你可以復制并粘貼子類代碼,而不需要理解它。我們將涵蓋大部分整本書中不同尋常的語法,但是讓我們一行一行地來看,以了解全貌。
```
@classmethod
```
這個修飾器將該方法標記為類方法。它本質上是說該方法可以被類而不是實例化對象調用:
```
def __subclasshook__(cls, C):
```
這定義了`__subclasshook__`類方法。這種特殊方法被Python解釋器調用,回答這個問題:*C類是這個類的子類嗎*?
```
if cls is MediaLoader:
```
我們檢查這個方法是否被這個類專門調用的,而不是,比如說這個類的子類。例如,這防止了`Wav`類被考慮作為`Ogg`類的父類:
```
attrs = set(dir(C))
```
這一行所做的只是獲取類擁有的一組方法和屬性,包括其類層次結構中的任何父類:
```
if set(cls.__abstractmethods__) <= attrs:
```
這一行使用集合符號來查看該類中的抽象方法集合是否已經在候選類中提供。請注意,它不會檢查是否這些方法已經實現,只是檢查它們是否存在。因此,一個類可能成為子類,但仍然是抽象類本身。
```
return True
```
如果已經提供了所有的抽象方法,那么候選類就是子類,我們返回真。該方法可以合法地返回三個值中的任意一個:真、假或未實現。真和假表示類是或者不是這個類的子類:
```
return NotImplemented
```
如果沒有滿足任何條件(也就是說,該類不是`MediaLoader`,或者沒有提供所有抽象方法),則返回`NotImplemented`。這告訴Python機器使用默認機制(候選類顯式擴展這個類?)用于子類檢測。
簡而言之,我們現在可以將Ogg類定義為`MediaLoader`類的子類,雖然我們沒有實際擴展`MediaLoader`類用于獲得`Ogg`類:
```
>>> class Ogg():
... ext = '.ogg'
... def play(self):
... print("this will play an ogg file")
...
>>> issubclass(Ogg, MediaLoader)
True
>>> isinstance(Ogg(), MediaLoader)
True
```
## 案例研究
讓我們試著用一個更大的例子把我們所學的一切聯系起來。我們會設計一個簡單的房地產應用程序,允許中介管理可供購買或出租的物業。有兩種類型的物業:公寓和住宅。中介需要能夠輸入一些有關新物業的相關細節,列出所有當前可用的物業,并將物業標記為出售或出租。為了簡單起見,我們先不用擔心編輯物業細節或重新激活一個售出的物業。
</b>
該項目將允許中介使用Python解釋器窗口與對象進行交互。在這個GUI和WEB應用的世界里,你可能想知道為什么我們要創建這樣老式的程序。簡單地說,窗口程序和web應用程序都需要大量的知識開銷和樣板代碼。如果我們使用這兩種模式中的任何一種,我們都會迷失在GUI編程或WEB編程中,而忽略了我們正在嘗試掌握的面向對象原則。
</b>
幸運的是,大多數GUI和WEB框架都使用面向對象的方法,我們現在研究的原理將有助于理解這些框架。我們將在第13章“并發性”中簡要討論這兩個框架,但完整細節遠遠超出了一本書的范圍。
</b>
看看我們的需求,似乎有相當多的名詞可以代表我們系統中的對象類別。顯然,我們需要代表一個物業的類。住宅和公寓可能需要單獨的類。租賃和購買也似乎需要單獨的代表。既然我們現在關注的是繼承,我們將研究如何使用繼承或多重繼承來共享行為。
</b>
住宅`House`和公寓`Apartment`都是物業類型,所以物業`Property`可以是這兩個類的超類。租賃`Rental`和購買`Purchase`需要一些額外的考慮;如果我們使用繼承,我們需要有單獨的類,例如,住宅租賃`HouseRental`和購買住宅`HousePurchase`的類,并使用多重繼承來合并它們。與基于組合或關聯的設計相比,這有點笨拙,但是讓我們跑跑看,看看我們能想出什么。
</b>
那么,哪些屬性可能與物業`Property`類相關聯呢?不管是公寓還是住宅,大多數人都會想知道面積、臥室和浴室數量。(有很多其他可以用來建模的屬性,但我們盡可能讓我們的原型保持簡單。)
</b>
如果物業是一棟住宅,我們可能會想登廣告宣傳房子有幾層,是否有車庫(附屬的、分離的或沒有),以及院子是否有圍欄。公寓則會顯示它是否有陽臺,洗衣房是套間的、硬幣、還是公共的?
</b>
兩種物業類型都需要一個方法來顯示物業特征。目前,沒有其他明顯的行為。
</b>
租賃物業將需要存儲每月租金,以及物業是否有家具,是否包括公用設施,如果不包括,租金將是多少。代售的物業需要存儲銷售價格和預估的年度物業稅。對于我們的應用程序,我們只需要顯示這些數據,這樣我們就可以只添加一個類似于其他類中使用的`display()`方法。
</b>
最后,我們需要一個中介`Agent`對象,它包含所有物業的列表,并顯示這些物業屬性,并允許中介創建新的物業。創建物業需要提示用戶輸入每種物業類型的相關詳細信息。這可以在中介`Agent`對象中完成,但是`Agent`需要知道許多物業類型的信息。這沒有利用多態性的優勢。另一種選擇是將提示放入每個類的初始化函數或者構造函數中,但這將不允許將來在GUI或WEB應用中使用這些類。更好的想法是創建一個靜態方法(譯注:Python in Nutshell P118,提到類級別的方法有兩個:靜態方法和類方法)來執行提示并返回提示參數的字典。然后,中介`Agent`所要做的就是提示用戶物業類型和支付方式,并要求正確的類進行實例化。
</b>
設計太多了!下面的類圖可以將我們的設計決策表達得更清楚一點:

哇,有很多繼承箭頭!我認為不可能再添加沒有交叉箭頭的繼承級別。多重繼承就是這樣一件麻煩的事情,甚至在設計階段。
</b>
這些類最棘手的方面是確保在繼承層次結構中正確得調用超類方法。讓我們從物業`Property`實現開始:
```
class Property:
def __init__(self, square_feet=' ', beds=' ',
baths=' ', **kwargs):
super().__init__(**kwargs)
self.square_feet = square_feet
self.num_bedrooms = beds
self.num_baths = baths
def display(self):
print("PROPERTY DETAILS")
print("================")
print("square footage: {}".format(self.square_feet))
print("bedrooms: {}".format(self.num_bedrooms))
print("bathrooms: {}".format(self.num_baths))
print()
def prompt_init():
return dict(square_feet=input("Enter the square feet: "),
beds=input("Enter number of bedrooms: "),
baths=input("Enter number of baths: "))
prompt_init = staticmethod(prompt_init)
```
這個類很簡單。我們已經`__init__`添加了額外的`**kwargs`參數,因為我們知道`**kwargs`將被用于多重繼承情況。我們還添加了`super()._init__`方法,以防我們不是多重繼承鏈中的最后一個調用(譯注:這句話的意思是,如果在多重繼承鏈中,不是最后一個被調用,在中間位置,又沒有使用`super`,是很危險的,會導致后面的參數都不會得到繼承)。在這種情況下,我們正在*消費*鍵參數,因為我們知道在其他繼承層次結構,這些鍵參數不是必須的。
</b>
我們在`prompt_init`方法中看到了一些新的東西。這種方法在最初創建后被設置成靜態方法。靜態方法只和類(類似于類變量)相關聯,而不是特定的對象實例。因此,他們沒有`self`變量。正因為如此,`super`關鍵詞將不起作用(沒有父對象,只有父類),所以我們簡單地在父類里直接調用靜態方法。此方法使用Python字典構造函數創建可以傳遞到`__init__`的值字典。每個鍵值通過調用`input`方法來提示輸入。(譯注:Python in Nutshell P118,提到靜態方法和類、類的實例相關的,所以這里有點疑問。例如下面這段代碼,類的實例也可以用靜態方法)
```
class AClass(object):
def astatic(): print('a static method')
astatic = staticmethod(astatic)
an_instance = AClass()
AClass.astatic()
a static method
an_instance.astatic()
a static method
```
公寓`Apartment`類擴展了物業`Property`,結構相似:
```
class Apartment(Property):
valid_laundries = ("coin", "ensuite", "none")
valid_balconies = ("yes", "no", "solarium")
def __init__(self, balcony=' ', laundry=' ', **kwargs):
super().__init__(**kwargs)
self.balcony = balcony
self.laundry = laundry
def display(self):
super().display()
print("APARTMENT DETAILS")
print("laundry: %s" % self.laundry)
print("has balcony: %s" % self.balcony)
def prompt_init():
parent_init = Property.prompt_init()
laundry = ' '
while laundry.lower() not in \
Apartment.valid_laundries:
laundry = input("What laundry facilities does "
"the property have? ({})".format(
", ".join(Apartment.valid_laundries)))
balcony = ' '
while balcony.lower() not in \
Apartment.valid_balconies:
balcony = input(
"Does the property have a balcony? "
"({})".format(
", ".join(Apartment.valid_balconies)))
parent_init.update({
"laundry": laundry,
"balcony": balcony
})
return parent_init
prompt_init = staticmethod(prompt_init)
```
`display()`和`__init__()`方法使用`super()`調用了相應的父類方法,確保物業`Property`類已被正確的初始化。
</b>
`prompt_init`靜態方法現在正在從父類獲取字典值,然后添加它自己的一些附加值。它調用`dict.update`方法將新字典值合并到第一個字典值中。然而,這個`prompt_init`方法看起來很難看;它循環兩次,直到用戶使用結構相似但變量不同的代碼,進行有效的輸入為止。提取這個驗證邏輯比較好,以便我們只在一個位置維護它;很可能對以后的類也很有用。
</b>
到目前為止,所有的討論都是關于繼承的,我們可能認為這是一個使用`mixin`的好地方。然而,在這種情況下,我們有機會研究繼承并不是最好的解決方案。我們想要創建的方法將被用在靜態方法中。如果我們打算繼承自一個提供驗證功能的類,該功能將也必須作為不訪問任何實例變量的靜態方法提供給新的類上。如果它不訪問任何實例變量,那么創建一個類有什么意義嗎?為什么我們不把這個驗證功能變成模塊級的函數呢,它接受一個輸入字符串和一個有效答案列表,并就此罷休?
</b>
讓我們探索一下這個驗證函數是什么樣子的:
```
def get_valid_input(input_string, valid_options):
input_string += " ({}) ".format(", ".join(valid_options))
response = input(input_string)
while response.lower() not in valid_options:
response = input(input_string)
return response
```
我們可以在解釋器中測試這個函數,獨立于我們擁有的所有其他類。這是一個好跡象,它意味著我們設計的不同部分不是彼此緊密耦合,并且以后可以獨立改進,且不影響其他代碼。
```
>>> get_valid_input("what laundry?", ("coin", "ensuite", "none"))
what laundry? (coin, ensuite, none) hi
what laundry? (coin, ensuite, none) COIN
'COIN'
```
現在,讓我們使用這個新函數快速更新我們的`Apartment.prompt_init`:
```
def prompt_init():
parent_init = Property.prompt_init()
laundry = get_valid_input(
"What laundry facilities does "
"the property have? ",
Apartment.valid_laundries)
balcony = get_valid_input(
"Does the property have a balcony? ",
Apartment.valid_balconies)
parent_init.update({
"laundry": laundry,
"balcony": balcony
})
return parent_init
prompt_init = staticmethod(prompt_init)
```
這更容易閱讀(也更容易維護!),比我們的原始版本更好。現在我們準備建造住宅類。這個類有一個與公寓`Apartment`平行的結構,但會引出不同的提示和變量:
```
class House(Property):
valid_garage = ("attached", "detached", "none")
valid_fenced = ("yes", "no")
def __init__(self, num_stories='',
garage='', fenced='', **kwargs):
super().__init__(**kwargs)
self.garage = garage
self.fenced = fenced
self.num_stories = num_stories
def display(self):
super().display()
print("HOUSE DETAILS")
print("# of stories: {}".format(self.num_stories))
print("garage: {}".format(self.garage))
print("fenced yard: {}".format(self.fenced))
def prompt_init():
parent_init = Property.prompt_init()
fenced = get_valid_input("Is the yard fenced? ",
House.valid_fenced)
garage = get_valid_input("Is there a garage? ",
House.valid_garage)
num_stories = input("How many stories? ")
parent_init.update({
"fenced": fenced,
"garage": garage,
"num_stories": num_stories
})
return parent_init
prompt_init = staticmethod(prompt_init)
```
這里沒有什么新的可探索的,所以讓我們轉到購買`Purchase` 和租賃`Rental`類上。盡管目的明顯不同,但它們和我們剛才討論的類,在設計上是相似的:
```
class Purchase:
def __init__(self, price='', taxes='', **kwargs):
super().__init__(**kwargs)
self.price = price
self.taxes = taxes
def display(self):
super().display()
print("PURCHASE DETAILS")
print("selling price: {}".format(self.price))
print("estimated taxes: {}".format(self.taxes))
def prompt_init():
return dict(
price=input("What is the selling price? "),
taxes=input("What are the estimated taxes? "))
prompt_init = staticmethod(prompt_init)
class Rental:
def __init__(self, furnished='', utilities='',
rent='', **kwargs):
super().__init__(**kwargs)
self.furnished = furnished
self.rent = rent
self.utilities = utilities
def display(self):
super().display()
print("RENTAL DETAILS")
print("rent: {}".format(self.rent))
print("estimated utilities: {}".format(
self.utilities))
print("furnished: {}".format(self.furnished))
def prompt_init():
return dict(
rent=input("What is the monthly rent? "),
utilities=input(
"What are the estimated utilities? "),
furnished = get_valid_input(
"Is the property furnished? ",
("yes", "no")))
prompt_init = staticmethod(prompt_init)
```
這兩個類沒有超類(除了`object`這個默認超類),但是我們仍然調用了`super().__init__`,因為它們將與其他類結合,并且我們不知道`super`方法會按什么順序調用。接口和用于住宅`House`和公寓`Apartment`的接口很相似,當我們在使用這四個類在不同子類中進行組合時,會很有幫助。例如:
```
class HouseRental(Rental, House):
def prompt_init():
init = House.prompt_init()
init.update(Rental.prompt_init())
return init
prompt_init = staticmethod(prompt_init)
```
這有點令人驚訝,因為類本身既沒有`__init__`也沒有`display`方法!因為兩個父類在這些方法中都適當地調用`super`,所以我們只需擴展這些類,這些類就會按照正確的順序運行。當然,`prompt_init`不是這種情況,因為它是一個不能調用`super`的靜態方法,所以我們顯式地實現了`prompt_init`。我們應該測試這個類,以確保在我們寫下其他三個組合之前,它的表現良好:
```
>>> init = HouseRental.prompt_init()
Enter the square feet: 1
Enter number of bedrooms: 2
Enter number of baths: 3
Is the yard fenced?
Is there a garage?
(yes, no) no
(attached, detached, none) none
How many stories? 4
What is the monthly rent? 5
What are the estimated utilities? 6
Is the property furnished?
(yes, no) no
>>> house = HouseRental(**init)
>>> house.display()
PROPERTY DETAILS
================
square footage: 1
bedrooms: 2
bathrooms: 3
HOUSE DETAILS
# of stories: 4
garage: none
fenced yard: no
RENTAL DETAILS
rent: 5
estimated utilities: 6
furnished: no
```
它看起來工作正常。`prompt_init`方法會提示我們初始化所有的超類,且`display()`也協同調用了所有三個超類。
> 前面示例中繼承的類的順序很重要。如果我們寫的是`class HouseRental(House, Rental)`,而不是`class HouseRental(Rental, House)`,`display()`將不會調用`Rental.display()`!當在我們版本的`HouseRental`上調用`display`時,它是指`Rental`版本的`display`方法,它調用`super.display()`以獲取`House`版本的`display()`,再次調用`super.display()`,將獲取物業`Property`版本的`display()`。如果我們反轉它,`display`指的是`House`類的`display()`。當調用`super`時,它調用`Property`父類上的方法。但`Property`在`display`方法中沒有對`super`的調用。這意味著`Rental`將不會調用類的`display`方法!通過按照我們所做的順序放置繼承列表,我們確保`Rental`調用`super`,然后處理`House`一方的繼承關系。你可能認為我們可以添加對`Property.display()`的`super`調用,但這是不行地,因為`Property`的下一個超類是`object`,而`object`是沒有`display`方法的。解決這個問題的另一個方法是允許`Rental`和`Purchase`來擴展`Property`類,而不是直接派生自`object`。(或者我們可以動態的修改方法解析順序,但這超出了本書的范圍。)
現在我們已經測試了它,我們準備創建其余部分的組合子類:
```
class ApartmentRental(Rental, Apartment):
def prompt_init():
init = Apartment.prompt_init()
init.update(Rental.prompt_init())
return init
prompt_init = staticmethod(prompt_init)
class ApartmentPurchase(Purchase, Apartment):
def prompt_init():
init = Apartment.prompt_init()
init.update(Purchase.prompt_init())
return init
prompt_init = staticmethod(prompt_init)
class HousePurchase(Purchase, House):
def prompt_init():
init = House.prompt_init()
init.update(Purchase.prompt_init())
return init
prompt_init = staticmethod(prompt_init)
```
這應該是我們遇到的最激烈的設計了!現在我們要做的就是創建中介`Agent`類,該類負責創建新列表并顯示現有的列表。讓我們從更簡單的物業存儲和列表開始:
```
class Agent:
def __init__(self):
self.property_list = []
def display_properties(self):
for property in self.property_list:
property.display()
```
添加物業需要首先查詢物業的類型,以及物業是用來購買或出租的。我們可以通過顯示一個簡單的菜單來做到這一點。一旦確定了這一點,我們就可以提取正確的子類,并使用我們已經開發的prompt_init層次結構提示所有細節。聽起來簡單嗎?是的。讓我們從向`Agent`類添加字典類變量開始:
```
type_map = {
("house", "rental"): HouseRental,
("house", "purchase"): HousePurchase,
("apartment", "rental"): ApartmentRental,
("apartment", "purchase"): ApartmentPurchase
}
```
那是一些看起來很有趣的代碼。這是一本字典,其中的鍵是元組,而值是類對象。類對象?是的,類可以像普通對象或原始數據類型一樣傳遞、重命名和存儲在容器中。有了這本簡單的字典,我們可以簡單地以多態形式調用我們之前的`get_valid_input`方法,以確保我們獲得正確的字典鍵并進行查找到合適的類,像這樣:
```
def add_property(self):
property_type = get_valid_input(
"What type of property? ",
("house", "apartment")).lower()
payment_type = get_valid_input(
"What payment type? ",
("purchase", "rental")).lower()
PropertyClass = self.type_map[
(property_type, payment_type)]
init_args = PropertyClass.prompt_init()
self.property_list.append(PropertyClass(**init_args))
```
這可能看起來也有點滑稽!我們在字典里查找這個類,并把它存儲在名為`PropertyClass`的變量。我們不知道哪個類可用,但是類自己知道,所以我們可以多態地調用`prompt_init`來獲得一個適合傳遞給構造函數的值字典。然后我們使用關鍵字參數語法將字典轉換為參數并構造新對象加載正確的數據。
</b>
現在,我們的用戶可以使用這個`Agent`類來添加和查看屬性列表。添加一些功能將物業標記為可用或不可用,或者編輯和刪除物業,并不需要做大量工作。我們的原型現在處于一個足夠好的狀態,并可以向一名真正的中介展示它的功能。看看演示是如何工作的:
```
>>> agent = Agent()
>>> agent.add_property()
What type of property?
What payment type?
(house, apartment) house
(purchase, rental) rental
Enter the square feet: 900
Enter number of bedrooms: 2
Enter number of baths: one and a half
Is the yard fenced?
Is there a garage?
(yes, no) yes
(attached, detached, none) detached
How many stories? 1
What is the monthly rent? 1200
What are the estimated utilities? included
Is the property furnished?
(yes, no) no
>>> agent.add_property()
What type of property?
What payment type?
(house, apartment) apartment
(purchase, rental) purchase
Enter the square feet: 800
Enter number of bedrooms: 3
Enter number of baths: 2
What laundry facilities does the property have?
(coin, ensuite,
one) ensuite
Does the property have a balcony? (yes, no, solarium) yes
What is the selling price? $200,000
What are the estimated taxes? 1500
>>> agent.display_properties()
PROPERTY DETAILS
================
square footage: 900
bedrooms: 2
bathrooms: one and a half
HOUSE DETAILS
# of stories: 1
garage: detached
fenced yard: yes
RENTAL DETAILS
rent: 1200
estimated utilities: included
furnished: no
PROPERTY DETAILS
================
square footage: 800
bedrooms: 3
bathrooms: 2
APARTMENT DETAILS
laundry: ensuite
has balcony: yes
PURCHASE DETAILS
selling price: $200,000
estimated taxes: 1500
```
## 摘要
我們已經脫離了簡單的繼承,這是程序員工具箱里面向對象中最有用的工具之一,一直到最復雜的多重繼承。繼承可以用來增加現有類或內置類型的功能。將類似的代碼抽象到父類中有助于增加可維護性。可以使用`super`和參數列表調用父類上的方法,使用多重繼承時,列表必須安全格式化,這些調用才能工作。
</b>
在下一章,我們將講述處理特殊情況的微妙藝術。
## 參考資料
[Python類方法、靜態方法與實例方法](https://www.cnblogs.com/blackmatrix/p/5606364.html)
[Python super() 函數](https://www.runoob.com/python/python-func-super.html)
[繼承與多態](https://www.liaoxuefeng.com/wiki/897692888725344/923030507728352)