18. 繼承
和面向?qū)ο缶幊套畛O嚓P(guān)的語言特性就是繼承 ( inheritance ) .
繼承值得是根據(jù)一個現(xiàn)有的類型 , 定義一個修改版本的新類的能力 .
本章中我會使用幾個類來表達(dá)撲克牌 , 牌組以及撲克牌性 , 用于展示繼承特性 . 如果你不玩撲克 , 可以在http : / / wikipedia . org / wiki / Poker里閱讀相關(guān)介紹 , 但其實(shí)并不必要 ;
我會在書中介紹練習(xí)中所需知道的東西 . 本章的代碼示例可以從https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Card . py下載 .
18.1 卡片對象
一副牌里有 52 張牌 , 共有 4 個花色 , 每種花色 13 張 , 大小各不相同 .
花色有黑桃 ( Spade ) , 紅桃 ( Heart ) , 方片 ( Diamond ) 和草花 ( Club ) ( 在橋牌中 , 這幾個花色是降序排列的 ) .
每種花色的 13 張牌分別為 : Ace , 2 , 3 , 4 , 5 , 6 , 7 , 7 , 9 , 10 , Jack , Queen , King .
根據(jù)你玩的不同游戲 , Ace可能比King大 , 可能比 2 小 . 如果我們定義一個新對象來表示卡牌 ,
則其屬性顯然應(yīng)該是rank ( 大小 ) 和suit ( 花色 ) . 但屬性的值就不那么直觀了 .
一種可能是使用字符串 , 例如 : 'Spade' 表示花色 , 用 'Queen' 表示大小 .
這種實(shí)現(xiàn)的問題之一是比較大小和花色的高低時會比較困難 . 另一種方案是使用整數(shù)類給大小和花色 '編碼' . 在這個語境中 , '編碼' 意味著我們要定義一個數(shù)字到花色 ,
或者數(shù)字到大小的隱射 . 這種編碼并不意味著它是密碼 ( 那樣就因該稱為 '加密' 了 ) . 例如 , 下表展示了花色和對應(yīng)的整數(shù)編碼 :
黑桃 Spades -- > 3
紅桃 Hearts -- > 2
方片 Diamonds -- > 1
草花 Clubs -- > 0
這個編碼令我們可以很容易地比較卡牌 ; 因?yàn)楦蟮幕ㄉ[射到更大的數(shù)字上 , 我們可以直接使用編碼來比較花色 .
卡牌大小的映射相當(dāng)明顯 ; 每個數(shù)字形式的大小映射到相應(yīng)的整數(shù)上 , 而對于花牌 :
Jack -- > 11
Queen -- > 12
King -- > 13
我使用 '->' 符號 , 是為了說明這些映射并不是Python程序的一部分 ,
它們是程序設(shè)計的一部分 , 但并不在代碼中直接表現(xiàn) .
Card類的定義如下 :
class Card : """Represents a standard playing card.(代表一張標(biāo)準(zhǔn)的撲克牌.)""" def __init__ ( self, suit= 0 , rank= 2 ) : self. suit = suitself. rank = rank
和前面一樣 , init方法對每個屬性定義一個可選形參 . 默認(rèn)的的卡牌是花草 2.
要創(chuàng)建一個Card對象 , 使用你想要的花色和大小調(diào)用Card :
queen_of_diaminds = Card( 1 , 13 )
18.2 類屬性
為了能將Card對象打印成人門容易約定的格式 , 我們需要將整數(shù)編碼映射成對應(yīng)的大小和花色 .
自然地做法是使用字符串列表 . 我們將這些列表賦值到 '類屬性' 上 :
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這樣的變量 , 定義在類之中 ,
但在任何方法之外的我們成為類屬性 , 因?yàn)樗鼈兪呛皖悓ο驝ard相關(guān)聯(lián)的 . 這個術(shù)語和suit與rank之類的變量相區(qū)別 .
那些稱為 '示例屬性' , 因?yàn)樗麄兪呛鸵粋€特定的實(shí)例相關(guān)聯(lián)的 . 兩種屬性都是使用句點(diǎn)表示法訪問 .
例如 , 在__str__中 , self是一個Card對象 , 而self . rank是它的大小 .
相似地 , Card是一個類對象 , 而Card . rank_names是關(guān)聯(lián)到這個類的一個字符串列表 . 每個卡牌都有它自己的suit和rank , 但總共只有一個suit_names和rank_names . 綜合起來 , 表達(dá)式Card . rank_names [ self.rank ] 意思是
'使用對象self的屬性rank作為索引, 從類Card的列表rank_names中選擇對應(yīng)的字符串' . rank_names的第一個元素是None , 因?yàn)闆]有小大為 0 的卡牌 . ( 讓索引對齊數(shù)字 , 更加直觀 . )
因?yàn)槭褂肗one占據(jù)了一個位置 , 我們就可以得到從下標(biāo) 2 到字符串 '2' 這樣整齊的映射 .
如果要避免這種操作 , 可以使用字典而不是列表 . 利用現(xiàn)有的方法 , 可以創(chuàng)建并打印開牌 :
>> > card1 = Card( 2 , 11 )
>> > print ( card1)
Jack of Hearts
class Card : """Represents a standard playing card.(代表一張標(biāo)準(zhǔn)的撲克牌.)""" suit_names = [ 'Clubs' , 'Diamonds' , 'Hearts' , 'Spades' ] rank_names = [ None , 'Ace' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '10' , 'Jack' , 'Queen' , 'King' ] def __init__ ( self, suit= 0 , rank= 2 ) : self. suit = suitself. rank = rankdef __str__ ( self) : return '%s of %s' % ( Card. rank_names[ self. rank] , Card. suit_names[ self. suit] ) card1 = Card( 2 , 11 )
print ( card1)
圖 18 - 1 展示了Card類對象和一個Card實(shí)例 .
Card是一個類對象 , 所以它的類型是type . card1的類型是Card .
為了節(jié)省空間 , 我沒有畫出suit_names , rank_names的內(nèi)容 .
18.3 對比卡牌
對應(yīng)內(nèi)置類型 , 我們比較操作符 ( < , > , = = 等 ) 來比較對象并決定哪一個更大 , 更小 , 或者相等 .
對應(yīng)用戶定義類型 , 我們可以通過提供一個方法__lt__ , 代碼 'less than' . 來重載內(nèi)置操作符的行為 . __lt__接收兩個形參 , self和other , 當(dāng)?shù)谝粋€對象嚴(yán)格小于第二個對象時返回True . 卡牌的正確順序并不顯而易見 . 例如 , 草花 3 和方塊 2 哪個更大?
一個排面數(shù)大 , 另一個花色大 . 為了比較卡牌 , 需要決定大小和花色哪個更重要 . 這個問題的答案取決去你在玩哪種牌類游戲 ,
但為了簡單起見 , 我們隨意做一個決定 , 認(rèn)為花色更重要 , 于是 , 所有的黑桃比所有的方片都大 , 依次類推 . 這一點(diǎn)決定后 , 我們就可以編寫__lt__函數(shù) :
def __lt__ ( self, other) : if self. suit < other. suit: return True if self. suit > other. suit: return False return self. rank < other. rank
使用元組比較 , 可以寫得更緊湊 :
def __lt__ ( self, other) : t1 = self. suit, self. rankt2 = other. suit, other. rankreturn t1 < t2
作為練習(xí) , 為時間對象編寫一個__lt__方法 .
你可以使用元組比較 ( 時 , 分 , 秒 ) , 也可以考慮使用整數(shù)比較 ( 時間對象轉(zhuǎn)為十進(jìn)制秒數(shù) ) .
class Time : def __init__ ( self, name, hour= 0 , minute= 0 , second= 0 ) : self. name = nameself. hour = hourself. minute = minuteself. second = seconddef __lt__ ( self, other) : print ( self. name, other. name) t1 = self. hour, self. minute, self. secondt2 = other. hour, other. minute, other. secondreturn t1 > t2def main ( ) : t1 = Time( 't1' , 9 , 45 , 0 ) t2 = Time( 't2' ) print ( t1 < t2) print ( t1 > t2) if __name__ == '__main__' : main( )
現(xiàn)在我們已經(jīng)有了卡牌 ( card ) , 下一步就是定義牌組 ( deck ) .
由于牌組是由卡牌組成的 , 很自然地 , 每個Deck對象因該有一個屬性包含卡牌的列表 .
class Deck : def __init__ ( self) : self. cards = [ ] for suit in range ( 4 ) : for rand in range ( 1 , 14 ) : card = Card( suit, rand) self. cards. append( card)
填充牌組最簡單的辦法是使用嵌套循環(huán) .
外層循環(huán)從 0 到 3 遍歷各個花色 . 內(nèi)層循環(huán)從 1 到 13 遍歷開牌大小 .
每次迭代使用當(dāng)前的花色和大小創(chuàng)建一個新的Card對象 , 并將它添加到self . cards中 .
18.5 打印牌組
下面是一個Deck的一個__str__方法 :
def __str__ ( self) : res = [ ] for card in self. cards: res. append( str ( card) ) return '\n' . join( res)
這個方案展示了一種累積構(gòu)建大字符串的方法 : 先構(gòu)建一個字符串的列表 , 再使用字符串方法join .
內(nèi)置函數(shù)str會對每個卡牌對每個卡牌對象調(diào)用__str__方法并返回字符串表達(dá)形式 .
( str ( 卡牌對象 ) , 會調(diào)用卡牌對象的__str__方法 . ) 由于我們對一個換行符調(diào)用join函數(shù) , 卡片之間用換行分隔 .
下面是打印的結(jié)果 :
>> > 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
雖然結(jié)果顯示了 52 行 , 它任然是一個包含換行符的字符串 .
class Card : """Represents a standard playing card.(代表一張標(biāo)準(zhǔn)的撲克牌.)""" suit_names = [ 'Clubs' , 'Diamonds' , 'Hearts' , 'Spades' ] rank_names = [ None , 'Ace' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '10' , 'Jack' , 'Queen' , 'King' ] def __init__ ( self, suit= 0 , rank= 2 ) : self. suit = suitself. rank = rankdef __str__ ( self) : return '%s of %s' % ( Card. rank_names[ self. rank] , Card. suit_names[ self. suit] ) def __lt__ ( self, other) : t1 = self. suit, self. rankt2 = other. suit, other. rankreturn t1 < t2class Deck : def __init__ ( self) : self. cards = [ ] for suit in range ( 4 ) : for rand in range ( 1 , 14 ) : card = Card( suit, rand) self. cards. append( card) def __str__ ( self) : res = [ ] for card in self. cards: res. append( str ( card) ) return '\n' . join( res) deck = Deck( )
print ( deck)
18.6 添加,刪除,洗牌和排序
為了能夠發(fā)牌 , 我們需要一個方法從牌組中抽取一張牌并返回 .
列表方法pop為此提供了一個方便的功能 :
def pop_card ( self) : return self. cards. pop( )
由于pop從列表中抽出最后一張牌 , 我們其實(shí)從牌組的低端發(fā)牌的 .
要添加一個卡牌 , 我們可以使用列表方法append :
def add_card ( self, card) : self. cards. append( card)
像這樣調(diào)用另一個方法 , 卻不做其他更多工作的方法 , 有時候稱為一個 '飾面(veneer)' .
這個比喻來自于木工行業(yè) , 在木工行業(yè)飾面是為了改善外觀而粘貼到便宜的木料表面的薄薄的一層優(yōu)質(zhì)木料 .
( 名字很高大尚 , 里面沒什么 . . )
在這個例子里 , add_card是一個 '薄薄' 的方法 , 用更適合牌組的術(shù)語來表達(dá)一個列表操作 .
它改善了實(shí)現(xiàn)的外觀 ( 或接口 ) . 作為另一個示例 , 我們可以使用random模塊的函數(shù)shuffle來編寫一個Deck方法shuffle ( 洗牌 ) :
def shuffle ( self) : random. shuffle( self. cards)
不要忘記導(dǎo)入random模塊 .
作為練習(xí) , 編寫一個Deck方法sort , 使用列表方法sord來對一個Deck中的卡牌進(jìn)行排序 .
sort使用我們定義的__lt__方法來決定順序 .
后面這句話的解釋 :
在Python中 , sort方法用于對列表進(jìn)行排序 .
默認(rèn)情況下 , sort方法會按照元素的大小順序來排序 , 而對于用戶自定義的類 ,
如果想要使用sort方法進(jìn)行排序 , 需要定義該類的比較方法 . 在本例中 , 我們定義了Card類 , 并在其中實(shí)現(xiàn)了__lt__方法 , 該方法用于比較兩張卡牌的大小 .
當(dāng)我們調(diào)用sort方法對Deck中的卡牌進(jìn)行排序時 , sort方法會自動調(diào)用Card類中的__lt__方法來比較卡牌的大小 ,
從而實(shí)現(xiàn)對卡牌的排序 . 因此 , 我們可以說 , sort方法使用了我們定義的__lt__方法來決定卡牌的順序 .
def sort ( self) : """按升序排列卡片.""" self. cards. sort( )
18.7 繼承
繼承是一個能夠定義一個新類對現(xiàn)有的某個類稍作修改的語言特性 .
作為示例 , 假設(shè)我們想要一個類來表達(dá)一副 '手牌' , 即玩家手握的一副牌 .
一副手牌和一套牌組相似 : 都是由卡牌的集合組成 , 并且都需要諸如增加和移除卡牌的操作 . 一副手牌和一套牌組也有區(qū)別 : 我們期望手牌擁有的一些操作 , 對牌組來說并無意義 .
例如 , 在撲克牌中 , 我們可能想要比較兩副手牌來判斷誰獲勝了 .
在橋牌中 , 我們可能需要為一副手牌計算分?jǐn)?shù)以叫牌 . 這種類之間的關(guān)系--相似 , 但不相同--讓它稱為繼承 .
要定義一個繼承現(xiàn)有類的新類 , 可以把現(xiàn)有類的名稱放在括號之中 :
class Hand ( Deck) : """Represents a hand of playing cards."""
這個定義說明Hand從Deck繼承而來 ;
這意味著我們可以像Deck對象那樣在Hand對象上使用pop_card和add_card方法 . 當(dāng)你類繼承現(xiàn)有類時 , 現(xiàn)有的類被稱為 '父類(parent)' , 而新類則稱為 '子類(child)' . 在本例中 , Hand也會繼承Deck的__init__方法 , 但它和我們想要的并不一樣 :
我們不需要填充 52 張卡牌 , Hand的init方法應(yīng)當(dāng)初始化cards為一個空列表 . 如果我們?yōu)镠and類提供了一個init方法 , 它會覆蓋Deck類的方法 :
def __init__ ( self, lable= '' ) : self. cards = [ ] self. lable = lable
在創(chuàng)建Hand對象時 , Python會調(diào)用這個init方法而不是Deck中的那個 :
>> > hand = Hand( 'new hand' )
>> > hand. cards
[ ]
>> > hand. label
'new hand'
其他的方法是從Deck中繼承而來的 , 所以我們可以使用pop_card和add_cards來出牌 .
>> > deck = Deck( )
>> > card = deck. pop_card( )
>> > hand. add_card( card)
print ( hand)
King of Spades
下一步很自然地就是將這段代碼封裝起來成為一個方法move_cards :
def move_cards ( self, hand, num) : for i in range ( num) : hand. add_card( self. pop_card( ) )
import randomclass Card : """Represents a standard playing card.(代表一張標(biāo)準(zhǔn)的撲克牌.)""" suit_names = [ 'Clubs' , 'Diamonds' , 'Hearts' , 'Spades' ] rank_names = [ None , 'Ace' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '10' , 'Jack' , 'Queen' , 'King' ] def __init__ ( self, suit= 0 , rank= 2 ) : self. suit = suitself. rank = rankdef __str__ ( self) : return '%s of %s' % ( Card. rank_names[ self. rank] , Card. suit_names[ self. suit] ) def __lt__ ( self, other) : t1 = self. suit, self. rankt2 = other. suit, other. rankreturn t1 < t2class Deck : def __init__ ( self) : self. cards = [ ] for suit in range ( 4 ) : for rand in range ( 1 , 14 ) : card = Card( suit, rand) self. cards. append( card) def __str__ ( self) : res = [ ] for card in self. cards: res. append( str ( card) ) return '\n' . join( res) def pop_card ( self) : return self. cards. pop( ) def add_card ( self, card) : self. cards. append( card) def shuffle ( self) : random. shuffle( self. cards) def sort ( self) : """按升序排列卡片.""" self. cards. sort( ) def move_cards ( self, hand, num) : for i in range ( num) : hand. add_card( self. pop_card( ) ) class Hand ( Deck) : """Represents a hand of playing cards.""" """當(dāng)子類未顯式調(diào)用父類的__init__()方法時, 會在子類提示該提示:Call to __init__ of super class is missed代碼并沒有錯誤, 只是提示用戶不要忘記調(diào)用父類初始化方法.""" def __init__ ( self, lable= '' ) : self. cards = [ ] self. lable = labledef main ( ) : deck = Deck( ) hand = Hand( 'new hand' ) deck. move_cards( hand, 13 ) for card in hand. cards: print ( card) if __name__ == '__main__' : main( )
move_cards接收兩個參數(shù) , 一個Hand對象以及需要出牌的牌數(shù) . 它會修改seld和hand , 返回None . 有的情況下 , 卡牌會從一副手牌中移除轉(zhuǎn)入到另一副手牌中 , 或者從手牌中回到牌組 .
你可以使用move_cards來處理全部這些操作 : self即可以是一個Deck對象 , 也可以是一個Hand對象 .
而hand參數(shù) , 雖然名字是hand卻也可以是一個Deck對象 .
繼承是很有用的語言特性 .
有些程序不用繼承些 , 會有很多重復(fù)代碼 , 使用繼承后就會更加優(yōu)雅 .
繼承也能促進(jìn)代碼復(fù)用 , 因?yàn)槟憧梢栽诓恍薷母割惖那疤嵯聦λ男袨檫M(jìn)行定制化 .
有的情況喜愛 , 繼承結(jié)構(gòu)反映了問題的自然結(jié)構(gòu) , 所以也讓設(shè)計更容易理解 . 但另一方面 , 繼承也可能會讓代碼更難讀 .
有時候當(dāng)一個方法被調(diào)用時 , 并不清楚到哪里能找到它的定義 .
相關(guān)的代碼可能散布在幾個不同的模塊中 .
并且 , 很多可以用繼承實(shí)現(xiàn)的功能 , 也能不用它實(shí)現(xiàn) , 甚至可以實(shí)現(xiàn)得更好 .
18.8 類圖
至此我們已見過用于顯示程序狀態(tài)的棧圖 , 以及用于顯示對象的屬性值的對象圖 .
這些圖表展示了程序運(yùn)行中的一個快照 , 所以當(dāng)程序繼續(xù)運(yùn)行時它們會跟著改變 . 它們也極其詳細(xì) ; 在某些情況下 , 是過于詳細(xì)了 . 而類圖對象結(jié)構(gòu)的展示相對來說更加抽象 .
它不會具體顯示每個對象 , 而是顯示各個類以及它們之間的關(guān)聯(lián) . 類之間有下面幾種關(guān)聯(lián) .
* 一個類的對象可以包含其他類的對象的引用 . 例如 , 米格Rectangle對象都包含一個到Point對象的引用 , 而每一個Deck對象包含到很多Card對象的引用 . 這種關(guān)聯(lián)稱為 'HAS-A(有一個)' , 也就是說 , '矩形(rectangle)中有一個點(diǎn)(Point)' . * 一個類可能繼承自另一個類 . 這種關(guān)系稱為IS-A ( 是一個 ) , 也就是說 '一副手牌(Hand)是一個牌組(Deck)' . * 一個類可能依賴于另一個類 . 也就是說 , 一個類的對象接收另一個類的對象作為參數(shù) , 或者使用另一個類的對象來進(jìn)行某種計算 . 這種關(guān)系稱為 '依賴(dependency)' . 類圖用圖形展示了這些關(guān)系 . 例如 , 下圖展示了Card , Deck和Hand之間的關(guān)系 .
空心三角形箭頭的線代表著一個IS-A關(guān)系 ; 這里表示Head是繼承自Deck的 . 標(biāo)準(zhǔn)的箭頭表示HAS-S關(guān)系 ; 這里表示Deck對象中用到Card對象的引用 . 箭頭附近的星號 ( * ) 表示是 '關(guān)聯(lián)重復(fù)標(biāo)記' ; 它表示Deck中有多個Cards .
這個數(shù)可以是一個簡單的數(shù)字 , 如 52 , 或者一個范圍 , 如 5. .7 , 或者一個星號 , 表示Deck可以有任意數(shù)量的Card引用 . 上圖中沒有任何依賴關(guān)系 . 依賴關(guān)系通常使用虛線箭頭表示 .
或者 , 如果有太多的依賴 , 有時候會忽略它們 . 更詳細(xì)的圖可能會顯示出Deck對象實(shí)際上包含了一個Card的列表 .
但在類圖中 , 像列表 , 字典這樣的內(nèi)置類型通常是不顯示的 .
18.9 數(shù)據(jù)封裝
前幾章展示了一個我們可以稱為 '面向?qū)ο笤O(shè)計' 的開發(fā)計劃 .
我們發(fā)現(xiàn)需要的對象 , 如Point , Rectangle和Time并定義類來表達(dá)它們 .
每個類都是一個對象到現(xiàn)實(shí)世界 ( 或者最少是數(shù)學(xué)世界 ) 中某種實(shí)體的明顯對應(yīng) . 但有時候你到底需要哪些對象 , 它們?nèi)绾谓换?/span>, 并不那么顯而易見 .
這時候你需要另一種開發(fā)計劃 .
和之前我們通過封裝和泛化來發(fā)現(xiàn)函數(shù)接口的方式相同 , 我們可以通過 '數(shù)據(jù)封裝' 來發(fā)現(xiàn)類的接口 . 13.8 節(jié)提供了一個很好的示例 . 如果從↓下載我的代碼 .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / markov . py
你會發(fā)現(xiàn)它使用了兩個全局變量 ( suffix_map和prefix ) 并且在多個函數(shù)中進(jìn)行讀寫 .
suffix_map = { }
prefix = ( )
因?yàn)檫@些變量是全局的 , 我們每次只能運(yùn)行一個分析 .
如果讀入兩個文本 , 它們的前綴和后綴就會添加到相同的數(shù)據(jù)結(jié)構(gòu)中 ( 最后可以用來產(chǎn)生一些有趣的文本 ) . 若要多此運(yùn)行分析 , 并保證他們之間的獨(dú)立 , 我們可以將每次分析的狀態(tài)信息封裝成一個對象 .
下面是它的樣子 :
class Markov : def __init__ ( self) : self. suffix_map = { } self. prefix = ( )
接下來我們將那些函數(shù)轉(zhuǎn)換為方法 .
例如 , 下面是process_word :
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 : self. suffix_map[ self. prefix] = [ word] self. prefix = shift( self. prefix, word)
像這樣轉(zhuǎn)換程序--修改設(shè)計單不修改其行為--是重構(gòu) ( 參見 4.7 節(jié) ) 的另一個示例 .
這個例子給出了一個設(shè)計對象和方法的開發(fā)計劃 .
* 1. 從編寫函數(shù) , ( 如果需要的話 ) 讀寫全局變量開始 .
* 2. 一旦你的程序能夠正確運(yùn)行 , 查看全局變量與使用它們的函數(shù)的關(guān)聯(lián) .
* 3. 將相關(guān)的變量封裝成對象的屬性 .
* 4. 將相關(guān)的函數(shù)轉(zhuǎn)換為這個新類的方法 .
作為練習(xí) , 從↓下載我的Markov代碼 , 并按照上面描述的步驟將全局變量封裝為一個叫作Markov的新類的屬性 .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / markov . py
解答 : https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Markov . py ( 注意M是大寫的 ) .
解答使用這個 : https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / markov2 . py
import sys
import random
suffix_map = { }
prefix = ( ) def process_file ( filename, order= 2 ) : fp = open ( filename) skip_gutenberg_header( fp) for line in fp: if line. startswith( '*** END OF THIS' ) : break for word in line. rstrip( ) . split( ) : process_word( word, order)
def skip_gutenberg_header ( fp) : for line in fp: if line. startswith( '*** START OF THIS' ) : break def process_word ( word, order= 2 ) : global prefixif len ( prefix) < order: prefix += ( word, ) return try : suffix_map[ prefix] . append( word) except KeyError: suffix_map[ prefix] = [ word] prefix = shift( prefix, word) def random_text ( n= 100 ) : start = random. choice( list ( suffix_map. keys( ) ) ) for i in range ( n) : suffixes = suffix_map. get( start, None ) if suffixes is None : random_text( n - i) return word = random. choice( suffixes) print ( word, end= ' ' ) start = shift( start, word) def shift ( t, word) : return t[ 1 : ] + ( word, )
def main ( script, filename= '158-0.txt' , n= 100 , order= 2 ) : try : n = int ( n) order = int ( order) except ValueError: print ( 'Usage: %d filename [# of words] [prefix length]' % script) else : process_file( filename, order) random_text( n) if __name__ == '__main__' : main( * sys. argv)
import sys
import random
def skip_gutenberg_header ( fp) : for line in fp: if line. startswith( '*** START OF THIS' ) : break
def shift ( t, word) : return t[ 1 : ] + ( word, )
class Markov : def __init__ ( self) : self. suffix_map = { } self. prefix = ( ) def process_file ( self, filename, order) : fp = open ( filename) skip_gutenberg_header( fp) for line in fp: if line. startswith( '*** END OF THIS' ) : break for word in line. rstrip( ) . split( ) : self. process_word( word, order) def process_word ( self, word, order) : if len ( self. prefix) < order: self. prefix += ( word, ) return try : self. suffix_map[ self. prefix] . append( word) except : self. suffix_map[ self. prefix] = [ word] self. prefix = shift( self. prefix, word) def random_text ( self, n= 100 ) : start = random. choice( list ( self. suffix_map. keys( ) ) ) for i in range ( n) : suffixes = self. suffix_map. get( start, None ) if suffixes is None : self. random_text( n - i) return word = random. choice( suffixes) print ( word, end= ' ' ) start = shift( start, word) def main ( script, filename= 'emma.txt' , n= 100 , order= 2 ) : try : n = int ( n) order = int ( order) except ValueError: print ( 'Usage: %d filename [# of words] [prefix length]' % script) else :
18.10 調(diào)試
繼承會給調(diào)試帶來新的挑戰(zhàn) , 因?yàn)楫?dāng)你調(diào)用對象的方法時 , 可無法知道調(diào)用的到底是哪個方法 .
假設(shè)你在編寫一個操作Hand對象的函數(shù) . 你可能希望能夠處理所有類型的Hand , 如PokerHands , BridgeHands等 .
如果你調(diào)用一個方法 , 如shuffle ( 排序 ) , 可能調(diào)用的是Decck中定義的方法 , 到如果任何子類重載了這個方法 ,
則你調(diào)用的會是那個重載的版本 . 一旦你無法確認(rèn)程序的運(yùn)行流程 , 最簡單的解決辦法是在相關(guān)的方法開頭添加一個打印語句 .
如果Deck . shuffle打印一句Running Deck . shuffle這樣的信息 , 則當(dāng)程序運(yùn)行時會跟蹤運(yùn)行的流程 . 或者 , 你也可以使用下面這個函數(shù) .
它接收一個對象和一個方法名 ( 字符串形式 ) , 并返回提供這個方法的定義的類 :
def find_defining_class ( obj, meth_name) : for ty in type ( obj) . mro( ) : if math_name in ty. __dict__: return ty
下面是使用的示例 :
>> > hand = Hand( )
>> > find_defining_class( hand, 'shuffle' )
< class 'Card.Deck' >
所以這個Hand對象的shuffle方法是在Deck類中定義的那個 . find_defining_class使用mro方法來獲得用于搜索調(diào)用方法的類對象 ( 類型 ) 列表 .
'MRO' 意思是 'method resolution order' ( 方法查找順序 ) , 是Python解析方法名稱的時候搜索的類的順序 . 一個設(shè)計建議 : 每次重載一個方法時 , 新方法的接口應(yīng)當(dāng)和舊方法的一致 .
它應(yīng)當(dāng)接收相同的參數(shù) , 返回相同的類型 , 并服從同樣的前置條件與后置條件 .
如果遵循這個規(guī)則 , 你會發(fā)現(xiàn)任何為如Deck這樣的父類設(shè)計的函數(shù) ,
都可以使用Hand或PokerHand這樣的子類的實(shí)例 . 如果你破壞這個也稱為 'Liskov替代原則' 的規(guī)則 , 你的代碼可能會像一堆 ( 不好意思 ) 紙牌屋一樣崩塌 .
18.11 術(shù)語表
編碼 ( encode ) : 使用一個集合的值來表示另一個集合的值 , 需要在它們之間建立映射 . 類屬性 ( class attribute ) : 關(guān)聯(lián)到類對象上的屬性 . 類屬性定義在類定義之中 , 但在所有方法定義之外 . 實(shí)例屬性 ( instance attribute ) : 和類的實(shí)例關(guān)聯(lián)的屬性 . 飾面 ( veneer ) : 一個方法或函數(shù) , 它調(diào)用另一個函數(shù) , 卻不做其他計算 , 只是為了提供不同的接口 . 繼承 ( inheritance ) : 可以定義一個新類 , 它是一個現(xiàn)有的類的修改版本 . 父類 ( parent class ) : 被子類所繼承的類 . 子類 ( child class ) : 通過繼承一個現(xiàn)有的類來創(chuàng)建的新類 , 也叫作 'subclass' . IS-A關(guān)聯(lián) ( IS-A relationship ) : 子類個父類之間的關(guān)聯(lián) . HAS-A關(guān)聯(lián) ( HAS-A relationship ) : 連個類之間的一種關(guān)聯(lián) : 一個類包含另一個類的對象的引用 . 依賴 ( dependency ) : 兩個類之間的一種關(guān)聯(lián) . 一個類的實(shí)例使用另一個類的實(shí)例 , 但不把它們作為屬性存儲起來 . 類圖 ( class diagram ) : 用來展示程序中的類以及它們之間的關(guān)聯(lián)的圖 . 重數(shù) ( multiplicity ) : 類圖中的一種標(biāo)記方法 , 對于HAS-A關(guān)聯(lián) , 用來表示一個類中有多少對另一個類的對象的引用 . 數(shù)據(jù)封裝 ( data encapsulation ) : 一個程序開發(fā)計劃 . 先使用全局變量來進(jìn)行原型設(shè)計 , 然后將全局變量轉(zhuǎn)換為實(shí)例屬性做出最終版本 .
18.12 練習(xí)
1. 練習(xí)1
針對下面的程序 , 畫一張UML類圖 , 展示這些類以及它們之間的關(guān)聯(lián) :
UML是什么?
統(tǒng)一建模語言 ( Unified Modeling Languag , UML ) 是一種為面向?qū)ο笙到y(tǒng)的產(chǎn)品進(jìn)行說明 ,
可視化和編制文檔的一種標(biāo)準(zhǔn)語言 , 是非專利的第三代建模和規(guī)約語言 .
UML是面向?qū)ο笤O(shè)計的建模工具 , 獨(dú)立于任何具體程序設(shè)計語言 .
class PingPongParent : pass class Ping ( PingPongParent) : def __init__ ( self, pong) : self. pong = pongclass Pong ( PingPongParent) : def __init__ ( self, pings= None ) : if pings is None : self. pings = [ ] else : self. pings = pingsdef add_ping ( self, ping) : self. pings. append( ping) pong = Pong( )
ping = Ping( pong)
pong. add_ping( ping)
這些類之間的關(guān)聯(lián)可以用以下方式表示 : * 1. Ping類IS-A PingPongParent類 , 即Ping類是PingPongParent類的子類 .
* 2. Pong類IS-A PingPongParent類 , 即Pong類也是PingPongParent類的子類 .
* 3. Ping類HAS-A Pong類的實(shí)例 , 即Ping類具有一個名為pong的屬性 , 保存一個Pong實(shí)例的引用 .
* 4. Pong類HAS-A Ping類的實(shí)例列表 , 即Pong類具有一個名為pings的屬性 , 保存多個Ping實(shí)例的列表 . ( 目前只有一個 , 則不使用 * 號 . ) 綜上所述 , Ping類和Pong類之間存在HAS-A關(guān)系 ,
而Ping類和PingPongParent類以及Pong類和PingPongParent類之間存在IS-A關(guān)系 .
2. 練習(xí)2
編寫一個名稱deal_hands的Deck方法 , 接收兩個形參 : 手牌的數(shù)量以及每副手牌的牌數(shù) .
它會根據(jù)形參創(chuàng)建新的Head對象 , 按照每副手牌的牌數(shù)出牌 , 并返回一個Hand對象列表 .
( 意思就是 , 發(fā)幾個人牌 , 每一副牌多少張 . )
import randomclass Card : """Represents a standard playing card.(代表一張標(biāo)準(zhǔn)的撲克牌.)""" suit_names = [ 'Clubs' , 'Diamonds' , 'Hearts' , 'Spades' ] rank_names = [ None , 'Ace' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '10' , 'Jack' , 'Queen' , 'King' ] def __init__ ( self, suit= 0 , rank= 2 ) : self. suit = suitself. rank = rankdef __str__ ( self) : return '%s of %s' % ( Card. rank_names[ self. rank] , Card. suit_names[ self. suit] ) def __lt__ ( self, other) : t1 = self. suit, self. rankt2 = other. suit, other. rankreturn t1 < t2class Deck : def __init__ ( self) : self. cards = [ ] for suit in range ( 4 ) : for rand in range ( 1 , 14 ) : card = Card( suit, rand) self. cards. append( card) def __str__ ( self) : res = [ ] for card in self. cards: res. append( str ( card) ) return '\n' . join( res) def pop_card ( self) : return self. cards. pop( ) def add_card ( self, card) : self. cards. append( card) def shuffle ( self) : random. shuffle( self. cards) def sort ( self) : """按升序排列卡片.""" self. cards. sort( ) def move_cards ( self, hand, num) : for i in range ( num) : hand. add_card( self. pop_card( ) ) def deal_hands ( self, hands, cards) : hands_list = [ ] self. shuffle( ) for i in range ( int ( hands) ) : hand = Hand( ) self. move_cards( hand, cards) hands_list. append( hand) return hands_listclass Hand ( Deck) : """Represents a hand of playing cards.""" """當(dāng)子類未顯式調(diào)用父類的__init__()方法時, 會在子類提示該提示:Call to __init__ of super class is missed代碼并沒有錯誤, 只是提示用戶不要忘記調(diào)用父類初始化方法.""" def __init__ ( self, lable= '' ) : self. cards = [ ] self. lable = labledef main ( hands, cards) : if hands * cards > 52 : print ( '超出52張牌了!' ) return deck = Deck( ) hand_list = deck. deal_hands( 4 , 4 ) for index, hand in enumerate ( hand_list) : index += 1 print ( '第%d個人的牌為:' % index) print ( hand) if __name__ == '__main__' : main( 4 , 4 )
"""
TypeError: add_card() missing 1 required positional argument: 'card'
類型錯誤:添加卡()失蹤1所需的位置參數(shù):“卡”
"""
3. 練習(xí)3
下面列出的是撲克牌中可能的手牌 , 按照牌值大小的增序 ( 也是可能性的降序 ) 排列 .
* 對子 ( pair ) : 兩張牌大小相同 .
* 兩對 ( two pair ) : 連個對子 .
* 三條 ( three of a Kind ) : 三張牌大小相同 .
* 順子 ( straight ) : 五張大小相連的牌 . ( Acc即可以是最大的也可以是最小 , 所以Acc- 2 - 3 - 4 - 5 是順子 , 10 -Jack-Queen-King-Acc也是 , 但Queen-King-Acc- 2 - 3 不是 ) .
* 同花 ( flush ) : 五張牌花色相同 .
* 滿堂紅 ( full house ) : 三張牌大小相同 , 另外兩張牌大小相同 .
* 四條 ( four of a Kind ) : 四張牌大小相同 .
* 同花順 ( straight flush ) : 順子 ( 如上面的定義 ) 里的五張牌都是花色相同的 .
本練習(xí)的目標(biāo)是預(yù)測這些手牌的出牌概率 .
1. 從↓下載這些文件 .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Card . py
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / PokerHand . py
* Card . py : 本章中介紹的Card , Deck和Hand類的完整代碼 .
* PokerHand . py : 表達(dá)撲克手牌的一個類 , 實(shí)現(xiàn)并不完整 , 包含一些測試它的代碼 .
import random
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 __init__ ( self, suit= 0 , rank= 2 ) : self. suit = suitself. rank = rankdef __str__ ( self) : return '%s of %s' % ( Card. rank_names[ self. rank] , Card. suit_names[ self. suit] ) def __eq__ ( self, other) : return self. suit == other. suit and self. rank == other. rankdef __lt__ ( self, other) : t1 = self. suit, self. rankt2 = other. suit, other. rankreturn t1 < t2
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) def __str__ ( self) : res = [ ] for card in self. cards: res. append( str ( card) ) return '\n' . join( res) def add_card ( self, card) : self. cards. append( card) def remove_card ( self, card) : self. cards. remove( card) def pop_card ( self, i= - 1 ) : return self. cards. pop( i) def shuffle ( self) : random. shuffle( self. cards) def sort ( self) : self. cards. sort( ) def move_cards ( self, hand, num) : for i in range ( num) : hand. add_card( self. pop_card( ) )
class Hand ( Deck) : def __init__ ( self, label= '' ) : self. cards = [ ] self. label = labelif __name__ == '__main__' : deck = Deck( ) deck. shuffle( ) hand = Hand( ) deck. move_cards( hand, 5 ) hand. sort( ) print ( hand)
2. 如果你運(yùn)行PokerHand . py , 它會連出 7 組包含 7 張卡片的撲克手牌 , 并檢查其中有沒有順子 ( 因該是同花 ) . 在繼續(xù)之前請仔細(xì)閱讀代碼 .
from Card import Hand, Deck
class PokerHand ( Hand) : def suit_hist ( self) : self. suits = { } for card in self. cards: self. suits[ card. suit] = self. suits. get( card. suit, 0 ) + 1 def has_flush ( self) : self. suit_hist( ) for val in self. suits. values( ) : if val >= 5 : return True return False if __name__ == '__main__' : deck = Deck( ) deck. shuffle( ) for i in range ( 7 ) : hand = PokerHand( ) deck. move_cards( hand, 7 ) hand. sort( ) print ( hand) print ( hand. has_flush( ) ) print ( '' )
3. 在PokerHand . py中添加方法 , has_pair ( 對子 ) , has_twopair ( 兩對 ) 等 . 它們根據(jù)手牌時候達(dá)到相對應(yīng)的條件來返回True或False . 你的代碼應(yīng)當(dāng)對任意數(shù)量的手牌都適用 ( 雖然最常見的手牌是 5 或者 7 ) .
from Card import Hand, Deck
class PokerHand ( Hand) : def suit_hist ( self) : self. suits = { } for card in self. cards: self. suits[ card. suit] = self. suits. get( card. suit, 0 ) + 1 def rank_hist ( self) : self. ranks = { } for card in self. cards: self. ranks[ card. rank] = self. ranks. get( card. rank, 0 ) + 1 def has_flush ( self) : self. suit_hist( ) for val in self. suits. values( ) : if val >= 5 : return True return False def has_pair ( self) : self. rank_hist( ) for val in self. ranks. values( ) : if val >= 2 : return True return False def has_twopair ( self) : pair_count = 0 self. rank_hist( ) for val in self. ranks. values( ) : if val >= 2 : pair_count += 1 if pair_count >= 2 : return True return False if __name__ == '__main__' : deck = Deck( ) deck. shuffle( ) for i in range ( 7 ) : hand = PokerHand( ) deck. move_cards( hand, 7 ) hand. sort( ) print ( hand) print ( '是否有同花:' , hand. has_flush( ) ) print ( '是否有對子:' , hand. has_pair( ) ) print ( '是否有連對:' , hand. has_twopair( ) ) print ( '' )
4. 編寫一個函數(shù)classsify ( 分類 ) , 它可以弄清楚一副手牌中出現(xiàn)最大的組合 , 并設(shè)置label屬性 . 例如 , 一副 7 張牌的手牌可能包含一個順子以及一個對象 ; 它應(yīng)當(dāng)標(biāo)記為 'flush' ( 順子 ) .
5. 但你確保分類方法可用時 , 下一步是預(yù)料各種手牌的概率 . 在PolerHand . py中編寫一個函數(shù) , 對一副牌進(jìn)行洗牌 , 將其分成不同手牌 , 對手牌進(jìn)行分類 , 并記錄每種分類出現(xiàn)的次數(shù) .
6. 打印一個表格 , 展示各種分類以及它們的概率 . 更多次地運(yùn)行你的程序 , 直到輸出收斂到一個合理程度的正確性為止 . 將你的結(jié)果和http : / / en . wikipedia . org / wiki / Hand_rankings上的值進(jìn)行對比 . 解答 : https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / PokerHandSoln . py
import random
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 __init__ ( self, suit= 0 , rank= 2 ) : self. suit = suitself. rank = rankdef __str__ ( self) : return '%s of %s' % ( Card. rank_names[ self. rank] , Card. suit_names[ self. suit] ) def __eq__ ( self, other) : return self. suit == other. suit and self. rank == other. rankdef __lt__ ( self, other) : t1 = self. suit, self. rankt2 = other. suit, other. rankreturn t1 < t2
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) def __str__ ( self) : res = [ ] for card in self. cards: res. append( str ( card) ) return '\n' . join( res) def add_card ( self, card) : self. cards. append( card) def remove_card ( self, card) : self. cards. remove( card) def pop_card ( self, i= - 1 ) : return self. cards. pop( i) def shuffle ( self) : random. shuffle( self. cards) def sort ( self) : self. cards. sort( ) def move_cards ( self, hand, num) : for i in range ( num) : hand. add_card( self. pop_card( ) )
class Hand ( Deck) : def __init__ ( self, label= '' ) : self. cards = [ ] self. label = labelif __name__ == '__main__' : deck = Deck( ) deck. shuffle( ) hand = Hand( ) deck. move_cards( hand, 5 ) hand. sort( ) print ( hand)
from Card import Hand, Deckclass Hist ( dict ) : """從每個項(xiàng)目(x)映射到其頻率。""" def __init__ ( self, seq= [ ] ) : for x in seq: self. count( x) def count ( self, x, f= 1 ) : self[ x] = self. get( x, 0 ) + fif self[ x] == 0 : del self[ x]
class PokerHand ( Hand) : """Represents a poker hand.""" all_labels = [ 'straightflush' , 'fourkind' , 'fullhouse' , 'flush' , 'straight' , 'threekind' , 'twopair' , 'pair' , 'highcard' ] def make_histograms ( self) : self. suits = Hist( ) self. ranks = Hist( ) for c in self. cards: self. suits. count( c. suit) self. ranks. count( c. rank) self. sets = list ( self. ranks. values( ) ) self. sets. sort( reverse= True ) def has_highcard ( self) : return len ( self. cards) def check_sets ( self, * t) : for need, have in zip ( t, self. sets) : if need > have: return False return True def has_pair ( self) : return self. check_sets( 2 ) def has_twopair ( self) : return self. check_sets( 2 , 2 ) def has_threekind ( self) : return self. check_sets( 3 ) def has_fourkind ( self) : return self. check_sets( 4 ) def has_fullhouse ( self) : return self. check_sets( 3 , 2 ) def has_flush ( self) : for val in self. suits. values( ) : if val >= 5 : return True return False def has_straight ( self) : ranks = self. ranks. copy( ) ranks[ 14 ] = ranks. get( 1 , 0 ) return self. in_a_row( ranks, 5 ) def in_a_row ( self, ranks, n= 5 ) : count = 0 for i in range ( 1 , 15 ) : if ranks. get( i, 0 ) : count += 1 if count == n: return True else : count = 0 return False def has_straightflush ( self) : s = set ( ) for c in self. cards: s. add( ( c. rank, c. suit) ) if c. rank == 1 : s. add( ( 14 , c. suit) ) """對于牌面為A的牌, 因?yàn)锳可以被視為1或14, 所以在集合s中需要同時添加(1, c.suit)和(14, c.suit)兩個元素, 以便在檢查同花順時能夠正確判斷.""" for suit in range ( 4 ) : count = 0 for rank in range ( 1 , 15 ) : if ( rank, suit) in s: count += 1 if count == 5 : return True else : count = 0 return False def has_straightflush ( self) : d = { } for c in self. cards: d. setdefault( c. suit, PokerHand( ) ) . add_card( c) for hand in d. values( ) : if len ( hand. cards) < 5 : continue hand. make_histograms( ) if hand. has_straight( ) : return True return False def classify ( self) : self. make_histograms( ) self. labels = [ ] for label in PokerHand. all_labels: f = getattr ( self, 'has_' + label) if f( ) : self. labels. append( label) class PokerDeck ( Deck) : def deal_hands ( self, num_cards= 5 , num_hands= 10 ) : hands = [ ] for i in range ( num_hands) : hand = PokerHand( ) self. move_cards( hand, num_cards) hand. classify( ) hands. append( hand) return handsdef main ( ) : lhist = Hist( ) n = 10000 for i in range ( n) : if i % 1000 == 0 : print ( i) deck = PokerDeck( ) deck. shuffle( ) hands = deck. deal_hands( 7 , 7 ) for hand in hands: for label in hand. labels: lhist. count( label) total = 7.0 * nprint ( total, 'hands dealt:' ) for label in PokerHand. all_labels: freq = lhist. get( label, 0 ) if freq == 0 : continue p = total / freqprint ( '%s happens one time in %.2f' % ( label, p) ) if __name__ == '__main__' : main( )