18. 继承
和面向对象编程最常相关的语言特性就是继承(inheritance).
继承值得是根据一个现有的类型, 定义一个修改版本的新类的能力.
本章中我会使用几个类来表达扑克牌, 牌组以及扑克牌性, 用于展示继承特性.
如果你不玩扑克, 可以在http://wikipedia.org/wiki/Poker里阅读相关介绍, 但其实并不必要;
我会在书中介绍练习中所需知道的东西.
本章的代码示例可以从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.
根据你玩的不同游戏, Ace可能比King大, 可能比2小.
如果我们定义一个新对象来表示卡牌,
则其属性显然应该是rank(大小)和suit(花色). 但属性的值就不那么直观了.
一种可能是使用字符串, 例如: 'Spade'表示花色, 用'Queen'表示大小.
这种实现的问题之一是比较大小和花色的高低时会比较困难.
另一种方案是使用整数类给大小和花色'编码'. 在这个语境中, '编码'意味着我们要定义一个数字到花色,
或者数字到大小的隐射. 这种编码并不意味着它是密码(那样就因该称为'加密'了).
例如, 下表展示了花色和对应的整数编码:
黑桃 Spades --> 3
红桃 Hearts --> 2
方片 Diamonds --> 1
草花 Clubs --> 0
这个编码令我们可以很容易地比较卡牌; 因为更大的花色隐射到更大的数字上, 我们可以直接使用编码来比较花色.
卡牌大小的映射相当明显; 每个数字形式的大小映射到相应的整数上, 而对于花牌:
Jack --> 11
Queen --> 12
King --> 13
我使用'->'符号, 是为了说明这些映射并不是Python程序的一部分,
它们是程序设计的一部分, 但并不在代码中直接表现.
Card类的定义如下:
class Card:
"""Represents a standard playing card.(代表一张标准的扑克牌.)"""
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
和前面一样, init方法对每个属性定义一个可选形参. 默认的的卡牌是花草2.
要创建一个Card对象, 使用你想要的花色和大小调用Card:
queen_of_diaminds = Card(1, 13)
18.2 类属性
为了能将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是它的大小.
相似地, Card是一个类对象, 而Card.rank_names是关联到这个类的一个字符串列表.
每个卡牌都有它自己的suit和rank, 但总共只有一个suit_names和rank_names.
综合起来, 表达式Card.rank_names[self.rank]意思是
'使用对象self的属性rank作为索引, 从类Card的列表rank_names中选择对应的字符串'.
rank_names的第一个元素是None, 因为没有小大为0的卡牌. (让索引对齐数字, 更加直观.)
因为使用None占据了一个位置, 我们就可以得到从下标2到字符串'2'这样整齐的映射.
如果要避免这种操作, 可以使用字典而不是列表.
利用现有的方法, 可以创建并打印开牌:
>>> card1 = Card(2, 11)
>>> print(card1)
Jack of Hearts
class Card:
"""Represents a standard playing 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 = suit
self.rank = rank
def __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实例.
Card是一个类对象, 所以它的类型是type. card1的类型是Card.
为了节省空间, 我没有画出suit_names, rank_names的内容.

18.3 对比卡牌
对应内置类型, 我们比较操作符(<, >, ==等)来比较对象并决定哪一个更大, 更小, 或者相等.
对应用户定义类型, 我们可以通过提供一个方法__lt__, 代码'less than'. 来重载内置操作符的行为.
__lt__接收两个形参, self和other, 当第一个对象严格小于第二个对象时返回True.
卡牌的正确顺序并不显而易见. 例如, 草花3和方块2哪个更大?
一个排面数大, 另一个花色大. 为了比较卡牌, 需要决定大小和花色哪个更重要.
这个问题的答案取决去你在玩哪种牌类游戏,
但为了简单起见, 我们随意做一个决定, 认为花色更重要, 于是, 所有的黑桃比所有的方片都大, 依次类推.
这一点决定后, 我们就可以编写__lt__函数:
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.rank
t2 = other.suit, other.rank
return t1 < t2
作为练习, 为时间对象编写一个__lt__方法.
你可以使用元组比较(时, 分, 秒), 也可以考虑使用整数比较(时间对象转为十进制秒数).
class Time:
def __init__(self, name, hour=0, minute=0, second=0):
self.name = name
self.hour = hour
self.minute = minute
self.second = second
def __lt__(self, other):
print(self.name, other.name)
t1 = self.hour, self.minute, self.second
t2 = other.hour, other.minute, other.second
return t1 > t2
def main():
t1 = Time('t1', 9, 45, 0)
t2 = Time('t2')
print(t1 < t2)
print(t1 > t2)
if __name__ == '__main__':
main()
现在我们已经有了卡牌(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)
填充牌组最简单的办法是使用嵌套循环.
外层循环从0到3遍历各个花色. 内层循环从1到13遍历开牌大小.
每次迭代使用当前的花色和大小创建一个新的Card对象, 并将它添加到self.cards中.
18.5 打印牌组
下面是一个Deck的一个__str__方法:
def __str__(self):
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)
这个方案展示了一种累积构建大字符串的方法: 先构建一个字符串的列表, 再使用字符串方法join.
内置函数str会对每个卡牌对每个卡牌对象调用__str__方法并返回字符串表达形式.
( str(卡牌对象), 会调用卡牌对象的__str__方法.)
由于我们对一个换行符调用join函数, 卡片之间用换行分隔.
下面是打印的结果:
>>> 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行, 它任然是一个包含换行符的字符串.
class Card:
"""Represents a standard playing 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 = suit
self.rank = rank
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank], Card.suit_names[self.suit])
def __lt__(self, other):
t1 = self.suit, self.rank
t2 = other.suit, other.rank
return t1 < t2
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)
def __str__(self):
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)
deck = Deck()
print(deck)
18.6 添加,删除,洗牌和排序
为了能够发牌, 我们需要一个方法从牌组中抽取一张牌并返回.
列表方法pop为此提供了一个方便的功能:
def pop_card(self):
return self.cards.pop()
由于pop从列表中抽出最后一张牌, 我们其实从牌组的低端发牌的.
要添加一个卡牌, 我们可以使用列表方法append:
def add_card(self, card):
self.cards.append(card)
像这样调用另一个方法, 却不做其他更多工作的方法, 有时候称为一个'饰面(veneer)'.
这个比喻来自于木工行业, 在木工行业饰面是为了改善外观而粘贴到便宜的木料表面的薄薄的一层优质木料.
(名字很高大尚, 里面没什么..)
在这个例子里, add_card是一个'薄薄'的方法, 用更适合牌组的术语来表达一个列表操作.
它改善了实现的外观(或接口).
作为另一个示例, 我们可以使用random模块的函数shuffle来编写一个Deck方法shuffle(洗牌):
def shuffle(self):
random.shuffle(self.cards)
不要忘记导入random模块.
作为练习, 编写一个Deck方法sort, 使用列表方法sord来对一个Deck中的卡牌进行排序.
sort使用我们定义的__lt__方法来决定顺序.
后面这句话的解释:
在Python中, sort方法用于对列表进行排序.
默认情况下, sort方法会按照元素的大小顺序来排序, 而对于用户自定义的类,
如果想要使用sort方法进行排序, 需要定义该类的比较方法.
在本例中, 我们定义了Card类, 并在其中实现了__lt__方法, 该方法用于比较两张卡牌的大小.
当我们调用sort方法对Deck中的卡牌进行排序时, sort方法会自动调用Card类中的__lt__方法来比较卡牌的大小,
从而实现对卡牌的排序.
因此, 我们可以说, sort方法使用了我们定义的__lt__方法来决定卡牌的顺序.
def sort(self):
"""按升序排列卡片."""
self.cards.sort()
18.7 继承
继承是一个能够定义一个新类对现有的某个类稍作修改的语言特性.
作为示例, 假设我们想要一个类来表达一副'手牌', 即玩家手握的一副牌.
一副手牌和一套牌组相似: 都是由卡牌的集合组成, 并且都需要诸如增加和移除卡牌的操作.
一副手牌和一套牌组也有区别: 我们期望手牌拥有的一些操作, 对牌组来说并无意义.
例如, 在扑克牌中, 我们可能想要比较两副手牌来判断谁获胜了.
在桥牌中, 我们可能需要为一副手牌计算分数以叫牌.
这种类之间的关系--相似, 但不相同--让它称为继承.
要定义一个继承现有类的新类, 可以把现有类的名称放在括号之中:
class Hand(Deck):
"""Represents a hand of playing cards."""
这个定义说明Hand从Deck继承而来;
这意味着我们可以像Deck对象那样在Hand对象上使用pop_card和add_card方法.
当你类继承现有类时, 现有的类被称为'父类(parent)', 而新类则称为'子类(child)'.
在本例中, Hand也会继承Deck的__init__方法, 但它和我们想要的并不一样:
我们不需要填充52张卡牌, Hand的init方法应当初始化cards为一个空列表.
如果我们为Hand类提供了一个init方法, 它会覆盖Deck类的方法:
def __init__(self, lable=''):
self.cards = []
self.lable = lable
在创建Hand对象时, Python会调用这个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 random
class Card:
"""Represents a standard playing 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 = suit
self.rank = rank
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank], Card.suit_names[self.suit])
def __lt__(self, other):
t1 = self.suit, self.rank
t2 = other.suit, other.rank
return t1 < t2
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)
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."""
"""
当子类未显式调用父类的__init__()方法时, 会在子类提示该提示:
Call to __init__ of super class is missed
代码并没有错误, 只是提示用户不要忘记调用父类初始化方法.
"""
def __init__(self, lable=''):
self.cards = []
self.lable = lable
def 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接收两个参数, 一个Hand对象以及需要出牌的牌数. 它会修改seld和hand, 返回None.
有的情况下, 卡牌会从一副手牌中移除转入到另一副手牌中, 或者从手牌中回到牌组.
你可以使用move_cards来处理全部这些操作: self即可以是一个Deck对象, 也可以是一个Hand对象.
而hand参数, 虽然名字是hand却也可以是一个Deck对象.
继承是很有用的语言特性.
有些程序不用继承些, 会有很多重复代码, 使用继承后就会更加优雅.
继承也能促进代码复用, 因为你可以在不修改父类的前提下对它的行为进行定制化.
有的情况喜爱, 继承结构反映了问题的自然结构, 所以也让设计更容易理解.
但另一方面, 继承也可能会让代码更难读.
有时候当一个方法被调用时, 并不清楚到哪里能找到它的定义.
相关的代码可能散布在几个不同的模块中.
并且, 很多可以用继承实现的功能, 也能不用它实现, 甚至可以实现得更好.
18.8 类图
至此我们已见过用于显示程序状态的栈图, 以及用于显示对象的属性值的对象图.
这些图表展示了程序运行中的一个快照, 所以当程序继续运行时它们会跟着改变.
它们也极其详细; 在某些情况下, 是过于详细了. 而类图对象结构的展示相对来说更加抽象.
它不会具体显示每个对象, 而是显示各个类以及它们之间的关联.
类之间有下面几种关联.
* 一个类的对象可以包含其他类的对象的引用.
例如, 米格Rectangle对象都包含一个到Point对象的引用, 而每一个Deck对象包含到很多Card对象的引用.
这种关联称为'HAS-A(有一个)', 也就是说, '矩形(rectangle)中有一个点(Point)'.
* 一个类可能继承自另一个类.
这种关系称为IS-A(是一个), 也就是说'一副手牌(Hand)是一个牌组(Deck)'.
* 一个类可能依赖于另一个类.
也就是说, 一个类的对象接收另一个类的对象作为参数, 或者使用另一个类的对象来进行某种计算.
这种关系称为'依赖(dependency)'.
类图用图形展示了这些关系. 例如, 下图展示了Card, Deck和Hand之间的关系.

空心三角形箭头的线代表着一个IS-A关系; 这里表示Head是继承自Deck的.
标准的箭头表示HAS-S关系; 这里表示Deck对象中用到Card对象的引用.
箭头附近的星号(*)表示是'关联重复标记'; 它表示Deck中有多个Cards.
这个数可以是一个简单的数字, 如52, 或者一个范围, 如5..7, 或者一个星号, 表示Deck可以有任意数量的Card引用.
上图中没有任何依赖关系. 依赖关系通常使用虚线箭头表示.
或者, 如果有太多的依赖, 有时候会忽略它们.
更详细的图可能会显示出Deck对象实际上包含了一个Card的列表.
但在类图中, 像列表, 字典这样的内置类型通常是不显示的.
18.9 数据封装
前几章展示了一个我们可以称为'面向对象设计'的开发计划.
我们发现需要的对象, 如Point, Rectangle和Time并定义类来表达它们.
每个类都是一个对象到现实世界(或者最少是数学世界)中某种实体的明显对应.
但有时候你到底需要哪些对象, 它们如何交互, 并不那么显而易见.
这时候你需要另一种开发计划.
和之前我们通过封装和泛化来发现函数接口的方式相同, 我们可以通过'数据封装'来发现类的接口.
13.8节提供了一个很好的示例.如果从↓下载我的代码.
https://github.com/AllenDowney/ThinkPython2/blob/master/code/markov.py
你会发现它使用了两个全局变量(suffix_map和prefix)并且在多个函数中进行读写.
suffix_map = {}
prefix = ()
因为这些变量是全局的, 我们每次只能运行一个分析.
如果读入两个文本, 它们的前缀和后缀就会添加到相同的数据结构中(最后可以用来产生一些有趣的文本).
若要多此运行分析, 并保证他们之间的独立, 我们可以将每次分析的状态信息封装成一个对象.
下面是它的样子:
class Markov:
def __init__(self):
self.suffix_map = {}
self.prefix = ()
接下来我们将那些函数转换为方法.
例如, 下面是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)
像这样转换程序--修改设计单不修改其行为--是重构(参见4.7节)的另一个示例.
这个例子给出了一个设计对象和方法的开发计划.
* 1. 从编写函数, (如果需要的话)读写全局变量开始.
* 2. 一旦你的程序能够正确运行, 查看全局变量与使用它们的函数的关联.
* 3. 将相关的变量封装成对象的属性.
* 4. 将相关的函数转换为这个新类的方法.
作为练习, 从↓下载我的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 prefix
if 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:
markov = Markov()
markov.process_file(filename, order)
markov.random_text(n)
if __name__ == '__main__':
main(*sys.argv)
18.10 调试
继承会给调试带来新的挑战, 因为当你调用对象的方法时, 可无法知道调用的到底是哪个方法.
假设你在编写一个操作Hand对象的函数. 你可能希望能够处理所有类型的Hand, 如PokerHands, BridgeHands等.
如果你调用一个方法, 如shuffle(排序), 可能调用的是Decck中定义的方法, 到如果任何子类重载了这个方法,
则你调用的会是那个重载的版本.
一旦你无法确认程序的运行流程, 最简单的解决办法是在相关的方法开头添加一个打印语句.
如果Deck.shuffle打印一句Running Deck.shuffle这样的信息, 则当程序运行时会跟踪运行的流程.
或者, 你也可以使用下面这个函数.
它接收一个对象和一个方法名(字符串形式), 并返回提供这个方法的定义的类:
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方法来获得用于搜索调用方法的类对象(类型)列表.
'MRO'意思是'method resolution order'(方法查找顺序), 是Python解析方法名称的时候搜索的类的顺序.
一个设计建议: 每次重载一个方法时, 新方法的接口应当和旧方法的一致.
它应当接收相同的参数, 返回相同的类型, 并服从同样的前置条件与后置条件.
如果遵循这个规则, 你会发现任何为如Deck这样的父类设计的函数,
都可以使用Hand或PokerHand这样的子类的实例.
如果你破坏这个也称为'Liskov替代原则'的规则, 你的代码可能会像一堆(不好意思)纸牌屋一样崩塌.
18.11 术语表
编码(encode): 使用一个集合的值来表示另一个集合的值, 需要在它们之间建立映射.
类属性(class attribute): 关联到类对象上的属性. 类属性定义在类定义之中, 但在所有方法定义之外.
实例属性(instance attribute): 和类的实例关联的属性.
饰面(veneer): 一个方法或函数, 它调用另一个函数, 却不做其他计算, 只是为了提供不同的接口.
继承(inheritance): 可以定义一个新类, 它是一个现有的类的修改版本.
父类(parent class): 被子类所继承的类.
子类(child class): 通过继承一个现有的类来创建的新类, 也叫作'subclass'.
IS-A关联(IS-A relationship): 子类个父类之间的关联.
HAS-A关联(HAS-A relationship): 连个类之间的一种关联: 一个类包含另一个类的对象的引用.
依赖(dependency): 两个类之间的一种关联. 一个类的实例使用另一个类的实例, 但不把它们作为属性存储起来.
类图(class diagram): 用来展示程序中的类以及它们之间的关联的图.
重数(multiplicity): 类图中的一种标记方法, 对于HAS-A关联, 用来表示一个类中有多少对另一个类的对象的引用.
数据封装(data encapsulation): 一个程序开发计划. 先使用全局变量来进行原型设计,
然后将全局变量转换为实例属性做出最终版本.
18.12 练习
1. 练习1
针对下面的程序, 画一张UML类图, 展示这些类以及它们之间的关联:
UML是什么?
统一建模语言(Unified Modeling Languag, UML)是一种为面向对象系统的产品进行说明,
可视化和编制文档的一种标准语言, 是非专利的第三代建模和规约语言.
UML是面向对象设计的建模工具, 独立于任何具体程序设计语言.
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)
这些类之间的关联可以用以下方式表示:
* 1. Ping类IS-A PingPongParent类, 即Ping类是PingPongParent类的子类.
* 2. Pong类IS-A PingPongParent类, 即Pong类也是PingPongParent类的子类.
* 3. Ping类HAS-A Pong类的实例, 即Ping类具有一个名为pong的属性, 保存一个Pong实例的引用.
* 4. Pong类HAS-A Ping类的实例列表, 即Pong类具有一个名为pings的属性, 保存多个Ping实例的列表.
(目前只有一个, 则不使用*号.)
综上所述, Ping类和Pong类之间存在HAS-A关系,
而Ping类和PingPongParent类以及Pong类和PingPongParent类之间存在IS-A关系.

2. 练习2
编写一个名称deal_hands的Deck方法, 接收两个形参: 手牌的数量以及每副手牌的牌数.
它会根据形参创建新的Head对象, 按照每副手牌的牌数出牌, 并返回一个Hand对象列表.
(意思就是, 发几个人牌, 每一副牌多少张.)
import random
class Card:
"""Represents a standard playing 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 = suit
self.rank = rank
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank], Card.suit_names[self.suit])
def __lt__(self, other):
t1 = self.suit, self.rank
t2 = other.suit, other.rank
return t1 < t2
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)
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_list
class Hand(Deck):
"""Represents a hand of playing cards."""
"""
当子类未显式调用父类的__init__()方法时, 会在子类提示该提示:
Call to __init__ of super class is missed
代码并没有错误, 只是提示用户不要忘记调用父类初始化方法.
"""
def __init__(self, lable=''):
self.cards = []
self.lable = lable
def 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所需的位置参数:“卡”
"""
3. 练习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): 顺子(如上面的定义)里的五张牌都是花色相同的.
本练习的目标是预测这些手牌的出牌概率.
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: 表达扑克手牌的一个类, 实现并不完整, 包含一些测试它的代码.
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 = suit
self.rank = rank
def __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.rank
def __lt__(self, other):
t1 = self.suit, self.rank
t2 = other.suit, other.rank
return 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 = label
if __name__ == '__main__':
deck = Deck()
deck.shuffle()
hand = Hand()
deck.move_cards(hand, 5)
hand.sort()
print(hand)
2. 如果你运行PokerHand.py, 它会连出7组包含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 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(两对)等.
它们根据手牌时候达到相对应的条件来返回True或False.
你的代码应当对任意数量的手牌都适用(虽然最常见的手牌是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. 编写一个函数classsify(分类), 它可以弄清楚一副手牌中出现最大的组合, 并设置label属性.
例如, 一副7张牌的手牌可能包含一个顺子以及一个对象; 它应当标记为'flush'(顺子).
5. 但你确保分类方法可用时, 下一步是预料各种手牌的概率.
在PolerHand.py中编写一个函数, 对一副牌进行洗牌, 将其分成不同手牌, 对手牌进行分类,
并记录每种分类出现的次数.
6. 打印一个表格, 展示各种分类以及它们的概率.
更多次地运行你的程序, 直到输出收敛到一个合理程度的正确性为止.
将你的结果和http://en.wikipedia.org/wiki/Hand_rankings上的值进行对比.
解答: 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 = suit
self.rank = rank
def __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.rank
def __lt__(self, other):
t1 = self.suit, self.rank
t2 = other.suit, other.rank
return 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 = label
if __name__ == '__main__':
deck = Deck()
deck.shuffle()
hand = Hand()
deck.move_cards(hand, 5)
hand.sort()
print(hand)
from Card import Hand, Deck
class Hist(dict):
"""从每个项目(x)映射到其频率。"""
def __init__(self, seq=[]):
for x in seq:
self.count(x)
def count(self, x, f=1):
self[x] = self.get(x, 0) + f
if 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的牌, 因为A可以被视为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 hands
def 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 * n
print(total, 'hands dealt:')
for label in PokerHand.all_labels:
freq = lhist.get(label, 0)
if freq == 0:
continue
p = total / freq
print('%s happens one time in %.2f' % (label, p))
if __name__ == '__main__':
main()