# 練習44.繼承Vs.包含
在有關英雄戰勝邪惡的童話中,總是有某種形式的黑暗森林。它可能是一個山洞,森林,另一個星球或者其他地方,每個人都知道英雄不應該去。當然,當不就之后壞人被你找到的時候,你發現,英雄已經去那個愚蠢的森林里殺壞人去了。看起來英雄進入了一種狀態,這種狀態要求英雄必須在這個邪惡的森林中冒險。
在面向對象編程中,繼承就是那個黑暗森林。有經驗的程序員知道要避免這種邪惡,因為他們知道,黑暗森林深處有著多重繼承這個邪惡的皇后。 她喜歡那她那龐大的牙齒吃掉軟件和程序員。但這個森林是如此強大,如此誘人,幾乎每一個程序員都必須進入它,與邪惡的皇后戰斗,盡量做到全身而退,才可以稱自己是真正的程序員。你不能抗拒的繼承森林的誘惑,所以你進去了。冒險之后,你試著遠離那個邪惡的森林,當你再次被迫進入的時候,你會攜帶一支軍隊進入(這段翻譯的太爛了,實在沒辦法把編程和童話聯系起來!)
這是一種有趣的方式來解說我要在這節練習教你的東西,它叫做繼承,當你使用它的時候,一定要當心再當心。正在森林里和女王戰斗的程序員可能告訴你你必須進去。他們這么說是因為他們需要你的幫忙,可能因為他們創建了太多他們已經無法掌控的東西。但是你一定要記得:
大多數繼承的用途,可以簡化或用組合物所取代,并應不惜一切代價避免多重繼承。
## 什么是繼承
繼承是用來描述一個類從它的父類那里獲得大部分甚至全部父類的功能。當你寫下`class Foo(Bar)`的時候,就發生了繼承,這句代碼的意思是“創建一個叫做Foo的類,并繼承Bar” 。當你執行這句的時候,編程語言使得`Foo`的實例所有的行為都跟`Bar`的實例一樣。這么做,可以讓你在類`Bar`中放一些通用功能,而那些需要特殊定制的函數或功能可以放在類 `Foo` 中。
當你需要做這些特殊化函數編寫的時候,父子類之間有3中交互方法:
> 1. 子類的方法隱性繼承父類方法
> 1. 子類重寫父類的方法
> 1. 對子類的操作改變父類
我將按順序給你展示這3種交互方式,并給你看它們的代碼。
## 隱性繼承
首先,我將給你展示的是隱性繼承發生在你在父類中定義了方法,而在子類中沒有:
~~~
class Parent(object):
def implicit(self):
print "PARENT implicit()"
class Child(Parent):
pass
dad = Parent()
son = Child()
dad.implicit()
son.implicit()
~~~
在`class Child`下使用`pass`的目的是告訴Python,在這里你想要一個空白的塊。這里創建了一個叫做`Child`的類,但是沒有在這個類中定義任何性的方法。它將從類`Parent`繼承獲得所有的方法。當你執行這段代碼的時候,你會看到:
~~~
$ python ex44a.py
PARENT implicit()
PARENT implicit()
~~~
注意一下,盡管我再代碼的13行調用了`son.implicit()`,而類`Child`并沒有一個定義叫做`implicit`的方法,代碼仍然正常工作了,因為它調用了`Parent`中定義的同名方法。這個高速你的是:如果你在基類(i.e., `Parent`)中定義了一個方法,那么所有的子類(i.e., `Child`) 都可以自動的獲得這個功能。對于你需要在很多類中有重復的方法來說,非常方便。
## 重寫方法
關于有些方法是隱式調用的問題原因在于有時候你想讓子類有不同的表現。這時你想重寫子類中的方法且有效的覆蓋父類中的方法。要做到這一點,你只需要在子類中定義一個同名的方法就行,例如
~~~
class Parent(object):
def override(self):
print "PARENT override()"
class Child(Parent):
def override(self):
print "CHILD override()"
dad = Parent()
son = Child()
dad.override()
son.override()
~~~
在這個例子中,兩個類中我都有一個叫做`override`的方法,所以讓我們來看看當你運行此例時都發生了什么
~~~
$ python ex44b.py
PARENT override()
CHILD override()
~~~
你可以看到,當第14行執行時,它執行了父類的`override`方法,因為變量`dad`是一個父類實例,但是當第15行執行時,打印的是子類的`override`方法,這是因為`son`是一個子類的實例,這個子類重寫了那個方法,定義了自己的版本。
休息以下,在繼續下面的內容之前,嘗試練習這兩種方法。
## 之前或之后改變父類
第三種使用繼承的方式比較特別,你想在父類版本的方法執行行為前后給出些提示,你首先像上個例子那樣重寫了方法,接著你使用一個Python內建的叫做`super`的方法得到了父類版本的方法調用。以下這個例子就是這么做的,你可以感受一下上面的描述是什么意思。
~~~
class Parent(object):
def altered(self):
print "PARENT altered()"
class Child(Parent):
def altered(self):
print "CHILD, BEFORE PARENT altered()"
super(Child, self).altered()
print "CHILD, AFTER PARENT altered()"
dad = Parent()
son = Child()
dad.altered()
son.altered()
~~~
重要的地方在于9-11行,在`son.altered()`被調用前我做了如下操作:
> 1. 因為我已經重寫了子類的`Child.altered`方法,第9行的運行結果應該和你的期待是一樣
> 1. 在這個例子中,我打算使用`super`來得到父類的`Parent.altered`版本。
> 1. 在第10行,我調用了`super(Child, self).altered()`方法, 這個方法能夠意識到繼承的發生,并給你獲得類`Parent`。你可以這樣讀這行代碼“調用`super`,參數為`Child`和`self`,然后不管它返回什么,調用方法`altered` ”
> 1. 在這種情況下,父類的`Parent.altered`版本執行,并打印出父類的信息。
> 1. 最后,代碼從`Parent.altered` 返回, `Child.altered` 方法繼續打印出剩下的信息。
運行了程序之后,你應該能看到下面的內容:
~~~
$ python ex44c.py
PARENT altered()
CHILD, BEFORE PARENT altered()
PARENT altered()
CHILD, AFTER PARENT altered()
~~~
## 三種組合使用
為了論證上面的三種方式,我有一個最終版本的文件,該文件給你展示了每一種繼承方式的交互:
~~~
class Parent(object):
def override(self):
print "PARENT override()"
def implicit(self):
print "PARENT implicit()"
def altered(self):
print "PARENT altered()"
class Child(Parent):
def override(self):
print "CHILD override()"
def altered(self):
print "CHILD, BEFORE PARENT altered()"
super(Child, self).altered()
print "CHILD, AFTER PARENT altered()"
dad = Parent()
son = Child()
dad.implicit()
son.implicit()
dad.override()
son.override()
dad.altered()
son.altered()
~~~
通讀每一行代碼,不管有沒有被重寫,寫一個注釋用來解釋每一行實現了什么,然后將代碼運行起來,確認你的結果和你的期望是否一致:
~~~
$ python ex44d.py
PARENT implicit()
PARENT implicit()
PARENT override()
CHILD override()
PARENT altered()
CHILD, BEFORE PARENT altered()
PARENT altered()
CHILD, AFTER PARENT altered()
~~~
## 使用`super()`的原因
這看起來應該是常識,但是隨后我們即將進入一個叫做所多重繼承的麻煩事。 多重繼承是指當你定義一個的類的時候,從一個或多個類繼承,例如:
~~~
class SuperFun(Child, BadStuff):
pass
~~~
上述代碼的意思是, "創建一個叫做`SuperFun`的類,它同時繼承類`Child` 和類 `BadStuff` ."
在這個例子中,只要你隱式的調用任何`SuperFun`的實例,Python把必須從類`Child`和`BadStuff`查詢可能的函數,但是,查找也需要一個順序。為了做到這點,Python使用"方法解析順序"(MRO)和一種叫做C3的運算法則來直接獲得。
因為MRO是復雜的,并使用了明確定義的算法,Python不能讓你來獲得正確的MRO,相反的,Python提供給你`super()`方法,它用來處理所有這一切你需要改變類型的行為,如同我在`Child.altered`所實現的。使用`super()`你不必擔心得到的是否是正確的方法,Python會幫你找到正確的那個。
## `__init__`中使用`super()`
`super()`最常見的用途是在基類的`__init__`方法里。這通常是你需要在子類里實現什么事情,然后完成父類初始化的地方。以下是在類`Child`中這樣做的一個簡單的例子:
~~~
class Child(Parent):
def __init__(self, stuff):
self.stuff = stuff
super(Child, self).__init__()
~~~
除了我在`__init__`中初始化父類之前定義了一些變量,這個跟上面的例子`Child.altered`幾乎是一樣的。
## 包含
繼承是有用的, 但另一種方式僅僅是用其他類和模塊就做到了同樣的事情, 而沒有使用隱性繼承。如果你看一下使用繼承的三種方式,其中的兩種方法涉及編寫新的代碼來替換或改變父類功能。這可以很容易地通過調用模塊函數復制。下面是一個例子:
~~~
class Other(object):
def override(self):
print "OTHER override()"
def implicit(self):
print "OTHER implicit()"
def altered(self):
print "OTHER altered()"
class Child(object):
def __init__(self):
self.other = Other()
def implicit(self):
self.other.implicit()
def override(self):
print "CHILD override()"
def altered(self):
print "CHILD, BEFORE OTHER altered()"
self.other.altered()
print "CHILD, AFTER OTHER altered()"
son = Child()
son.implicit()
son.override()
son.altered()
~~~
在這段代碼中,我沒有使用名字`Parent`,因為沒有父子的`is-a`關系了。這是一個`has-a`關系,在這個關系中`Child``has-a``Other`被用來保證代碼的正常工作。當我運行代碼時,看到以下輸出:
~~~
$ python ex44e.py
OTHER implicit()
CHILD override()
CHILD, BEFORE OTHER altered()
OTHER altered()
CHILD, AFTER OTHER altered()
~~~
你可以看到`Child`和`Other`中的大部分代碼實現了相同的功能。唯一的不同之處在于我必須定義一個 `Child.implicit` 方法去做一個動作. 然后我可以問問自己,如果我需要一個`Other`類,是不是只要把它放在一個叫做`other.py`的模塊中就可以?
## 什么時候用繼承,什么時候用包含
繼承與包含的問題可以歸結為試圖解決可重復使用代碼的問題。你不想在你的軟件中有重復的代碼,因為這不是高效的干凈的代碼。繼承通過創建一種機制,讓你在基類中有隱含的功能來解決這個問題。而包含則是通過給你的模塊和函數可以在其他類別被調用來解決這個問題。
如果這兩種方案都能解決代碼復用問題的話,哪一個更合適呢?答案是主觀的,這看起來是令人難以相信的,但是我會給你3個指導性原則:
> 1. 不惜一切代價避免多重繼承,因為它太復雜太不可靠。如果你必須要使用它,那么一定要知道類的層次結構,并花時間找到每一個類是從哪里來的。
> 1. 將代碼封裝為模塊,這樣就可以在許多不同的地方或情況使用。
> 1. 只有當有明顯相關的可重用的代碼,且在一個共同概念下時,可以使用繼承。
不要變成規則的奴隸。關于面向對象編程要記住的是:這是一個程序員創建打包和共享代碼的社會習俗。因為它是一個社會習俗,但在Python的法典中,你可能會因為跟你合作的人而被迫避開這些規則。在這種情況下,了解他們是如何使用規則的,然后去適應形勢。
## 附加題
這節練習中只有一個附加題,因為這其實是一個很大的練習。閱讀 `http://www.python.org/dev/peps/pep-0008/` 并嘗試將它應用到你的代碼中。你會發現,有些內容跟你從這本書學到的不同,但是現在你應該能夠理解他們的建議,并將其應用到自己的代碼中。本書中剩余部分的代碼可能會也可能不會遵循這些準則,這個要取決于這些準則是否會使代碼更加混亂。我建議你也這樣做,因為理解比讓大家都對你深奧的知識有印象更重要。
## 常見問題
### Q: 我如何更好的解決我以前沒有遇到的問題?
> 更好的解決問題的辦法只有一個,那就是盡量多的自己解決遇到的問題。通常,人們遇到一個棘手的問題,就會沖出去尋找答案。當你必須要把事情做好的時候,這種方法很好,但是如果你有時間的話,最好還是自己想辦法解決這個問題。盡你所能的思考這個問題,嘗試一切可能的辦法,直到你解決這個問題。在此之后你找到的答案,你覺得會更加令人滿意,而且你還會得到更好的解決問題的方法。
### Q: 對象不是復制的類嗎?
> 在某些語言里(比如 JavaScript) 是這樣的。這些被稱為原型的語言,這些語言中對象和類沒有什么不同之處。然而在Python中,類作為模板,可以生成新對象,類似于如何使用模具制造硬幣。
- 序言
- 前言
- 簡介
- 0:安裝和準備
- 1:第一個程序
- 2:注釋和“#”井號
- 3:數字和數學計算
- 4:變量和命名
- 5:更多的變量和打印
- 6:字符串和文本
- 7:更多的打印(輸出)
- 8:打印, 打印
- 9:打印, 打印, 打印
- 10:那是什么?
- 11:提問
- 12:提示別人
- 13:參數, 解包, 變量
- 14:提示和傳遞
- 15:讀文件
- 16:讀寫文件
- 17:更多文件操作
- 18:命名, 變量, 代碼, 函數
- 19:函數和變量
- 20:函數和文件
- 21:函數的返回值
- 22:到目前為止你學到了什么?
- 23:閱讀代碼
- 24:更多的練習
- 25:更多更多的練習
- 26:恭喜你,可以進行一次考試了
- 27:記住邏輯
- 28:布爾表達式
- 29:IF 語句
- 30:Else 和 If
- 31:做出決定
- 32:循環和列表
- 33:while循環
- 34:訪問列表元素
- 35:分支和函數
- 36:設計和調試
- 37:復習符號
- 38:列表操作
- 39:字典,可愛的字典
- 40:模塊, 類和對象
- 41:學會說面向對象
- 42:對象、類、以及從屬關系
- 43:基本的面向對象的分析和設計
- 44:繼承Vs.包含
- 45:你來制作一個游戲
- 46:項目骨架
- 47:自動化測試
- 48:更復雜的用戶輸入
- 49:寫代碼語句
- 50:你的第一個網站
- 51:從瀏覽器獲取輸入
- 52:開始你的web游戲
- 來自老程序員的建議
- 下一步
- 附錄A:命令行教程
- 簡介
- 安裝和準備
- 路徑, 文件夾, 名錄 (pwd)
- 如果你迷路了
- 創建一個路徑 (mkdir)
- 改變當前路徑 (cd)
- 列出當前路徑 (ls)
- 刪除路徑 (rmdir)
- 目錄切換(pushd, popd)
- 生成一個空文件(Touch, New-Item)
- 復制文件 (cp)
- 移動文件 (mv)
- 查看文件 (less, MORE)
- 輸出文件 (cat)
- 刪除文件 (rm)
- 退出命令行 (exit)
- 下一步