# 第十八章 繼承
面向對象編程最常被人提到的語言功能就是繼承了。繼承就是基于一個已有的類進行修改來定義一個新的類。在本章我會用一些例子來演示繼承,這些例子會用到一些類來表示撲克牌,成副的紙牌和撲克牌型。
如果你沒玩過撲克,你可以讀一下[這里的介紹](http://en.wikipedia.org/wiki/Poker),不過也沒必要;因為我等會會把練習中涉及到的相關內容給你解釋明白的。
本章的代碼樣例可以在[這里](http://thinkpython2.com/code/Card.py)下載。
## 18.1 紙牌對象
牌桌上面一共有 52 張撲克牌,每一張都屬于四種花色之一,并且是十三張牌之一。花色為黑桃,紅心,方塊,梅花(在橋牌中按照降序排列)。排列順序為 A,2,3,4,5,6,7,8,9,10,J,Q,K。根據具體玩的游戲的不同,A 可以比 K 大,也可以比 2 還小。
如果咱們要定義一個新的對象來表示一張牌,很明顯就需要兩個屬性了:點數以及花色。但這兩個屬性應該是什么類型呢,就不那么明顯了。一種思路是用字符串,就比如用『黑桃』來表示花色,『Q』來表示點數。不過這個實現方法不怎么方便,不好去比較紙牌的點數大小以及花色。
另外一種思路是用整數來編碼,以表示點數和花色。在這里,『編碼』的意思就是我們要建立一個從數值到花色或者從數值到點數的映射。這種編碼并不是為了安全的考慮(那種情況下用的詞是『encryption(也是編碼的意思,專用于安全領域)』)。
例如,下面這個表格就表示了花色與整數編碼之間的映射關系:
```py
Spades ? 3
Hearts ? 2
Diamonds ? 1
Clubs ? 0
```
這樣的編碼就比較易于比較牌的大小;因為高花色對應著大數值,我們對比一下編碼大小就能比較花色順序。
牌面大小的映射就很明顯了;每一張牌都對應著相應大小的整數,對于有人像的幾張映射如下所示:
```py
Jack ? 11
Queen ? 12
King ? 13
```
我這里用箭頭符號 ? 來表示映射關系,但這個符號并不是 Python 所支持的。這些符號是程序設計的一部分,但最終并不以這種形式出現在代碼里。
這樣實現的紙牌類的定義如下所示:
```py
class Card:
"""Represents a standard playing card."""
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
```
一如既往,init 方法可以為每一個屬性接收一個可選參數來初始化。默認的牌面為梅花 2.
要建立一張紙牌,可以用你想要的花色和牌值調用 Card。
```py
queen_of_diamonds = Card(1, 12)
```
## 18.2 類的屬性
想要以易于被人理解的方式來用 print 打印輸出紙牌對象,我們就得建立一個從整形編碼到對應的牌值和花色的映射。最自然的方法莫過于用字符串列表來實現。咱們可以先把這些列表賦值到類的屬性中去:
```py
# inside class Card:
suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7','8', '9', '10', 'Jack', 'Queen', 'King']
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank],Card.suit_names[self.suit])
```
suit_names 和 rank_names 這樣的變量,都是在類內定義,但不在任何方法之內,這就叫做類的屬性,因為它們屬于類 Card。
這種形式就把類的屬性與變量 suit 和 rank 區分開來,后面這兩個變量叫做實例屬性,因為這兩個屬性取決于具體的實例。
這些屬性都可以用點號來讀取。比如,在 __str__ 方法中,self 是一個 Card 對象,而 self.rank 就是該對象的 rank 變量。同理,Card 是一個 class 對象,而 Card.rank_names 就是屬于該類的一個字符串列表。
沒一張牌都有自己的花色和牌值,但都只有唯一的一套 suit_names 和 rank_names。
放到一起,這個表達式 Card.rank_names[self.rank]的意思就是『用對象 self 的 rank 屬性作為一個索引,從類 Card 中的 rank_names 列表中選擇該索引位置的字符串。』
rank_names 的一個元素是 None 空,因為沒有牌值為 0 的紙牌。包含 None 在內作為一個替位符,整個映射就很簡明,索引 2 的位置對應著就是字符串「2」,其他牌值依此類推。要是覺得這樣太別扭,咱們還可以用字典來替代列表。
目前已經有了這些方法了,咱們就可以創建和打印輸出紙牌了:
```py
>>> card1 = Card(2, 11)
>>> print(card1)
Jack of Hearts
```
________________________________________

Figure 18.1: Object diagram.
________________________________________
圖 18.1 是一個 Card 類對象以及一個 Card 實例的圖解。Card 是一個類對象(就是類的一個實例);它的類型是 type。card1 是 Card 的一個實例,所以它的類型是 Card。為了節省空間,我沒有畫出 suit_names 和 rank_names 的內容。
## 18.3 對比牌值
對于內置類型,直接就可以用關系運算符(<, >, ==,等等)比較兩個值來判斷二者的大小以及是否相等。對與用戶自定義類型,咱們就要覆蓋掉內置運算符的行為,這就需要提供一個名為 __lt__ 的方法,這個 lt 就是『less than』的縮寫,意思是『小于』。
__lt__ 接收兩個參數,一個是 self,一個是另外一個對象,如果 self 嚴格小于另外一個對象,就返回真。
紙牌的牌值大小排列并不是很簡單。比如,梅花 3 和方塊 2 哪個更大呢?一個的牌值更高,但另一個的花色更高。所以要進行比較的話,你就得確定牌值和花色哪個更重要。
實際上這種關系還得取決于你玩的紙牌游戲中的規則,不過為了簡單起見,咱們就做一個武斷的選擇,就讓花色更重要,所以所有的黑桃都大于方塊,依此類推了。
確定好規則了,就可以寫這個 __lt__ 方法了:
```py
# inside class Card:
def __lt__(self, other):
# check the suits
if self.suit < other.suit:
return True
if self.suit > other.suit:
return False
# suits are the same... check ranks
return self.rank < other.rank
```
用元組對比就可以把代碼寫得更簡潔了:
```py
# inside class Card:
def __lt__(self, other):
t1 = self.suit, self.rank
t2 = other.suit, other.rank
return t1 < t2
```
做個練習,為 Time 對象寫一個 __lt__ 方法。可以用元組對比,不過也可以對比整數。
## 18.4 Decks 成副的紙牌
現在咱們已經有了紙牌的類了,接下來的一不就是定義成副紙牌了。因為一副紙牌上是有各種牌,所以很自然就應該包含一個紙牌列表作為一個屬性了。
下面就是一個一副紙牌類的定義。init 方法建立了一個屬性 cards,然后生成了標準的五十二張牌來初始化。
```py
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)
```
實現一副牌的最簡單方法就是用網狀循環了。外層循環枚舉花色從 0 到 3 一共四種。內層的循環枚舉從 1 到 13 的所有牌值。每一次循環都以當前的花色和牌值創建一個新的 Card 對象,添加到 self.cards 列表中。
## 18.5 輸出整副紙牌
下面是 Deck 類的 __str__ 方法:
```py
# inside class Deck:
def __str__(self):
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)
```
上面的方法展示了累積大字符串的一種有效方法:建立一個字符串列表,然后用字符串方法 join 實現。內置函數 str 調用每一張牌的 __str__ 方法,然后返回該張紙牌的字符串表示。
Since we invoke join on a newline character, the cards are separated by newlines. Here’s what the result looks like:
由于我們調用 join 的位置在換行符后面,這樣這些紙牌就被換行符分開了。程序運行結果如下所示:
```py
>>> deck = Deck()
>>> print(deck)
Ace of Clubs
2 of Clubs
3 of Clubs
...
10 of Spades
Jack of Spades
Queen of Spades
King of Spades
```
雖然結果看上去是 52 行,但實際上只是一個包含了很多換行符的一個長字符串。
## 18.6 添加,刪除,洗牌和排序
要處理紙牌,我們還需要一個方法來從牌堆中拿出和放入紙牌。列表的 pop 方法很適合來完成這件任務:
```py
# inside class Deck:
def pop_card(self):
return self.cards.pop()
```
pop 方法從列表中拿走最后一張牌,這樣就是從一副牌的末尾來處理。
要添加一張牌,可以用列表的 append 方法:
```py
# inside class Deck:
def add_card(self, card):
self.cards.append(card)
```
上面這種方法都是調用了其他的方法,而沒有做什么別的事情,所以也被叫做鑲板。這個比喻來自于木匠行業,鑲板就是一薄層的高端木料用膠水貼到廉價木料上面,來提高視覺效果。
在剛剛的例子中,add_card 就相當于那個『高端』的方法,表示的是適用于處理紙牌的列表操作。這樣就提高了程序實現的可讀性,或者說改善了接口。
再舉一個例子,咱們再來給 Deck 寫一個洗牌的方法,用 random(隨機的意思)模塊的 shuffle 方法:
```py
# inside class Deck:
def shuffle(self):
random.shuffle(self.cards)
```
一定別忘了導入 random 模塊。
做個練習吧,寫一個名為 sort 的方法給 Deck,使用列表的 sort 方法來給 Deck 中的牌進行排序。sort 方法要用到我們之前寫過的 __lt__ 方法來確定順序。
## 18.7 繼承
I
繼承就是基于已有的類進行修改來獲取新類的能力。舉個例子,比方說我們需要一個表示『一手牌』的類,這個就是指一個牌手手中拿著的牌。『一手牌』和『一副牌』有些相似:都是由一系列的紙牌組成的,也都要有添加和移除紙牌的運算。
『一手牌』還和『一副牌』有所區別;對于手中的牌有一些運算并不適用于整副的牌。比如說,在撲克游戲中,我們可能需要對比兩手牌來看看哪一副勝利。在橋牌里面,還可能需要對手中的牌進行計分以決勝負。
類之間這種相似又有區別的關系,就適合用繼承來實現了。要繼承一個已有的類來定義新類,就要把已有類的名字放到括號中,如下所示:
```py
class Hand(Deck):
"""Represents a hand of playing cards."""
```
上面這樣的定義就表示了 Hand 繼承了 Deck;也就意味著我們可以在 Hands 中使用 Decks 中的那些方法,比如 pop_card 以及 add_card 等等。
當一個新類繼承了一個已有的類時,這個已有的類就叫做基類,新定義的類叫做子類。
在本章的這個例子中,Hand 類從 Deck 類繼承了 __init__ 方法,但這個方法和我們的需求還不一樣:Hand 類的 init 方法應該用一個空列表來初始化手中的牌,而不是像 Deck 類中那樣用一整副 52 張牌。
```py
# inside class Hand:
def __init__(self, label=''):
self.cards = []
self.label = label
```
像上面這樣改寫一下之后,這樣再建立一個 Hand 類的時候,Python 就會調用這個自定義的 init 方法,而不是 Deck 當中的。
```py
>>> hand = Hand('new hand')
>>> hand.cards []
>>> hand.label
'new hand'
```
其他方法都從 Deck 類中繼承了過來,所以我們就可以直接用 pop_card 和 add_card 方法來處理紙牌了:
```py
>>> deck = Deck()
>>> card = deck.pop_card()
>>> hand.add_card(card)
>>> print(hand)
King of Spades
```
接下來很自然地,我們把這段名為 move_cards 的方法放進去:
```py
# inside class Deck:
def move_cards(self, hand, num):
for i in range(num):
hand.add_card(self.pop_card())
```
move_cards 方法接收兩個參數,一個 Hand 對象,以及一個要處理的紙牌數量。該方法會修改 self 和 hand。返回為空。
在有的游戲中,紙牌需要從一手牌拿出去放到另外一手牌中去,或者從手中拿出去放到牌堆里面。這就虧用 move_cards 來實現這些操作:第一個變量 self 可以是一副牌也可以是一手牌,第二個變量雖然名字是 hand,實際上也可以是一個 Deck 對象。
繼承是一個很有用的功能。有的程序如果不用繼承的話就會有很多重復代碼,用繼承來寫出來就會更簡潔很多了。繼承有助于代碼重用,因為你可以對基類的行為進行定制而不用去修改基類本身。在某些情況下,繼承的結構也反映了要解決的問題中的自然關系,這就讓程序設計更易于理解。
然而繼承也容易降低程序可讀性。當調用一個方法的時候,有時候不容易找到該方法的定義位置。相關的代碼可能跨了好幾個模塊。此外,很多事情可以用繼承來實現,但不用繼承也能做到同樣效果,甚至做得更好。
## 18.8 類圖
目前為止,我們見過棧圖了,棧圖是展示一個程序的狀態的,我們還見過對象圖了,表示的是一個對象中的各個屬性及其值。這些圖都是對一個程序運行中某個瞬間的反映,因此隨著程序運行而產生變化。
這些圖解還都非常詳細;有的時候就都過于繁瑣冗余了。而類圖則是對一個程序結構的更抽象的表示。類圖并不會表現出各個獨立的對象,而是會表現出程序中的各個類以及它們之間的關系。
類之間有很多種關系,大概如下所示:
? 一個類中的對象可能包含了另一個類對象的引用。例如,每一個 Rectangle (矩形)對象都包含了對 Point(點)的引用,而每一個 Deck (成副的牌)對象都包含了對很多個 Card (紙牌)對象的引用。這種關系也叫做『含有』,就好比是說,『一個矩形中含有一個點。』
? 一類可能繼承了其他的類。這種關系也可以叫做『是一個』,比如說,『一手牌就是一種牌的組合。』
? 一種類可能要依賴其他類,比如一個類中的對象用另外一個類中的對象作為參數,或者用做計算中的某一部分。這種關系就叫做『依賴』。
類圖就是對這些關系的一個圖形化的表示。比如,在途 18.2 中,就展示了 Card,Deck 以及 Hand 三個類的關系。
________________________________________

Figure 18.2: Class diagram.
________________________________________
有空心三角形的箭頭表示了『是一個』的關系;在這里意思就是 Hand 繼承了 Deck。
另一個箭頭表示了『有一個』的關系;在這里的意思是 Deck 當中有若干對 Card 對象的引用。
箭頭處有個小星號*;這里可以表明一個 Deck 中含有的 Card 的個數。可以標出個數,比如 52,或者是范圍,比如 5..7 或者一個星號,這就意味著一個 Deck 中可以含有任意個數的 Card。
這個圖解中沒有出現依賴關系。這種關系一般用虛線箭頭來表示。或者當依賴關系很多的時候,有時候就都忽略掉了。
更細節化的圖解就可能表現出一個 Deck 中會包含一個 Card 對象組成的列表,但一般情況下類圖不會包括內置類型比如列表和字典。
## 18.9 調試
繼承可以讓調試變得很夸你呢,因為你調用某個對象中的某個方法的時候,很難確定到底是調用的哪一個方法。
假設你寫一個處理 Hand 對象的函數。你可能要讓該函數適用于所有類型的牌型,比如常規牌型,橋牌牌型等等。假設你要調用洗牌的方法 shuffle,你可能用的是 Deck 類當中的,不過如果子類當中有覆蓋的該方法,你運行的就是子類中的方法了。這種行為一般是很有好處的,不過也容易把人弄糊涂。
在你的程序運行的過程中,只要你對程序流程有疑問了,就可以在相關的方法頭部添加 print 語句來打印輸出一下信息,這就是最簡單的解決方法了。如果 Deck.shuffle 輸出了信息比如說『在運行 Deck 的 shuffle』,那就可以根據這些信息來追蹤執行流程了。
另外一個思路,就是用下面這個函數,該函數接收一個對象和一個方法的名字(作為字符串),然后返回提供該方法定義的類的名稱。
```py
def find_defining_class(obj, meth_name):
for ty in type(obj).mro():
if meth_name in ty.__dict__:
return ty
```
如下所示:
```py
>>> hand = Hand()
>>> find_defining_class(hand, 'shuffle')
<class 'Card.Deck'>
```
所這樣就能判斷這里面 Hand 中的 shuffle 方法是來自 Deck 的。
find_defining_class 用了 mro 方法來獲取所有搜索方法的類對象的列表。
『MRO』的意思是『method resolution order(方法 解決方案 順序)』,也就是 Python 搜索來找到方法名的類的序列。
H
下面是一個在設計上的建議:當你覆蓋一個方法的時候,新的方法的接口最好同舊的完全一致。應該接收同樣的參數,返回同樣類型,并且遵循同樣的前置條件和后置條件。只要你遵守這個規則,你就會發現所有之前設計來處理一個基類的函數,比如處理 Deck 的,就都可以用于子類的實例上面,比如 Hand 類或者 PokerHand 類。
如果你違背了上面這個『里氏替換原則』,你的代碼就可能很悲劇地崩潰,就像無數紙牌坍塌一樣。
## 18.10 數據封裝
之前的章節中,我們展示了所謂『面向對象設計』的開發規劃模式。在這些章節中,我們顯示確定好需要的對象—比如點,矩形以及時間—然后再定義一些類去代表這些內容。在這些例子中,類的對象與現實世界(或者至少是數學世界)中的一些實體都有顯著的對應關系。
不過有時候就不太好確定具體需要什么樣的對象,以及如何去實現。這時候就需要一種完全不同的開發規劃模式了。之前我們對函數接口進行過封裝和泛化的處理,現在也可以通過數據封裝來改進類的接口。
比如馬科夫分析,在 13.8 中出現的,就是一個很好的例子。如果你從[這里](http://thinkpython2.com/code/markov.py)下載了我的樣例代碼,你就會發現這里用了兩個全局變量—suffix_map 以及 prefix—這兩個全局變量會被多個函數讀取和寫入。
```py
suffix_map = {}
prefix = ()
```
這些變量是全局的,因此我們每次只運行了一次分析。如果我們要讀取兩個文本,他們的前置和后置詞匯都會被添加到同樣的數據結構上面去(這樣就能生成一些有趣的機器制造的文本了)。
如果要運行多次分析,并且要對這些分析進行區分,我們可以把每次分析的狀態封裝到對象中。如下所示:
```py
class Markov:
def __init__(self):
self.suffix_map = {}
self.prefix = ()
```
接下來就是把各個函數轉換成方法。例如下面就是 process_word 方法:
```py
def process_word(self, word, order=2):
if len(self.prefix) < order:
self.prefix += (word,)
return
try:
self.suffix_map[self.prefix].append(word)
except
KeyError: # if there is no entry for this prefix, make one
self.suffix_map[self.prefix] = [word]
self.prefix = shift(self.prefix, word)
```
上面這種方式對程序進行的修改只是改變了設計,而不改變程序的行為,這就是重構的另一個例子(參考 4.7)。
這一樣例展示了一種設計累的對象和方法的開發規劃模式:
1. 先開始寫一些函數來讀去和寫入全局變量(在必要的情況下)。
2. 一旦程序可以工作了,就檢查一下全局變量與使用它們的函數之間的關系。
3. 把相關的變量作為類的屬性封裝到一起。
4. 把相關的函數轉換成新類的方法。
做一個練習,從[這里](http://thinkpython2.com/code/markov.py)下載我的馬科夫分析代碼,然后根據上面說的步驟來一步步把全局變量封裝成一個名為 Markov 的新類的屬性。[樣例代碼](http://thinkpython2.com/code/Markov.py) (一定要注意 M 是大寫的哈)
## 18.11 Glossary 術語列表
encode:
To represent one set of values using another set of values by constructing a mapping between them.
>編碼:通過建立映射的方式來用一系列的值來表示另外一系列的值。
class attribute:
An attribute associated with a class object. Class attributes are defined inside a class definition but outside any method.
>類的屬性:屬于某個類的對象的屬性。類的屬性都在類定義的內部,在類內方法之外。
instance attribute:
An attribute associated with an instance of a class.
>實例屬性:屬于某個類的實例的屬性。
veneer:
A method or function that provides a different interface to another function without doing much computation.
>嵌板:某一方法或者函數,為另外的函數提供了不同的接口,而沒有做額外運算。
inheritance:
The ability to define a new class that is a modified version of a previously defined class.
>繼承:基于已定義過的類,進行修改來定義一個新類,這種特性就是繼承。
parent class:
The class from which a child class inherits.
>基類:被子類繼承的類。
child class:
A new class created by inheriting from an existing class; also called a “subclass”.
>子類:基于已有類而建立的新類;也稱為『分支類』。
IS-A relationship:
A relationship between a child class and its parent class.
>『是一個』關系:一種子類與基類之間的關系。
HAS-A relationship:
A relationship between two classes where instances of one class contain references to instances of the other.
>『有一個』關系:某一個類的實例中包含其他類的實例的引用的關系。
dependency:
A relationship between two classes where instances of one class use instances of the other class, but do not store them as attributes.
>依賴關系:兩個類之間的一種關系,一個類的實例使用了另外一個類的實例,但并未作為屬性來存儲。
class diagram:
A diagram that shows the classes in a program and the relationships between them.
>類圖:一種展示程序中各個類及其之間關系的圖解。
multiplicity:
A notation in a class diagram that shows, for a HAS-A relationship, how many references there are to instances of another class.
>多樣性:類圖中顯示的一種記號,適用于『有一個』關系中,表示一個類當中另一個類的實例的引用的個數。
data encapsulation:
A program development plan that involves a prototype using global variables and a final version that makes the global variables into instance attributes.
>數據封裝:一種程序開發規劃方式,用全局變量做原型體,然后逐步將這些全局變量轉換成實例的屬性。
## 18.12 練習
### 練習 1
閱讀下面的代碼,畫一個 UML 類圖,表示出程序中的類,以及類之間的關系。
```py
class PingPongParent:
pass class Ping(PingPongParent):
def __init__(self, pong):
self.pong = pong
class Pong(PingPongParent):
def __init__(self, pings=None):
if pings is None:
self.pings = []
else:
self.pings = pings
def add_ping(self, ping):
self.pings.append(ping)
pong = Pong()
ping = Ping(pong)
pong.add_ping(ping)
```
### 練習 2
為 Deck 類寫一個名為 deal_hands 的方法,接收兩個參數,一個為牌型數量,一個為每一個牌型的紙牌數。該方法需要創建適當的牌型對象數量,處理適當的每個牌型中的紙牌數,然后返回一個牌型組成的列表。
### 練習 3
下面是撲克牌中可能的各個牌型,排列順序為值的升序,出現概率的降序:
pair:
two cards with the same rank
>一對:兩張同樣牌值的牌
two pair:
two pairs of cards with the same rank
>雙對:兩對同樣牌值的牌
three of a kind:
three cards with the same rank
>三張:三張同樣牌值的牌
straight:
five cards with ranks in sequence (aces can be high or low, so Ace-2-3-4-5 is a straight and so is 10-Jack-Queen-King-Ace, but Queen-King-Ace-2-3 is not.)
>順子:五張牌值連續的牌(A 可以用作開頭,也可以用作結尾,所以 A-2-3-4-5 是一個順子,10-J-Q-K-A 也是一個,但 Q-K-A-2-3 就不行了。)
flush:
five cards with the same suit
>同花:五張牌花色一致
full house:
three cards with one rank, two cards with another
>三帶二:三張同牌值的牌,兩張另外的同牌值的牌
four of a kind:
four cards with the same rank
>四條:四張同一牌值的牌
straight flush:
five cards in sequence (as defined above) and with the same suit
>同花順:五張組成順子并且是同一花色的牌
此次練習的目的就是要估計獲得以上各個牌型的概率。
1. 從[這個網址](http://thinkpython2.com/code)下載下面的文件:
:Card.py
: 該文件是本章所涉及的 Card,Deck 以及 Hand 類的完整實現。
PokerHand.py
: 該文件是一個不完整版本的類,表示的是一個牌型,以及一些測試代碼。
2. 如果你運行 PokerHand.py,改程序會處理七個七張牌的牌型,然后檢查是否其中包含一副順子。
好好閱讀一下這份代碼,然后再繼續后面的練習。
3. >在 PokerHand.py 里面增加名為 has_pair, has_twopair 等等方法。這些方法根據牌型中是否滿足特定的組合而返回 True 或者 False。你的代碼應該能適用于有任意張牌的牌型(雖然 5 或者 7 是最常見的牌數)。
4. Write a method named classify that figures out the highest-value classification for a hand and sets the label attribute accordingly. For example, a 7-card hand might contain a flush and a pair; it should be labeled “flush”.
寫一個名為 classify 的函數,判斷出一副牌型中的最高值的一份,然后用來命名到標簽屬性。例如,一個七張牌的牌型可能包含一個順子和一個對子;這就應該被標為『順子』。
5. 當你確定你的分類方法運轉正常了,下一步就是要估計各個牌型的出現概率。在 PokerHand.py 中寫一個函數來對一副牌進行洗牌,分成多個牌型,對各個牌型進行分類,然后統計不同類型出現的次數。
6. 打印輸出一個由類型和概率組成的列表。逐步用大規模的牌型來測試你的程序,直到輸出的值趨向于一個比較合理的準確范圍。把你的運行結果與[這里的結果](http://en.wikipedia.org/wiki/Hand_rankings)進行對比。
[樣例代碼](http://thinkpython2.com/code/PokerHandSoln.py)