18. 第十八章 继承

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
        # 大小默认为2 
        self.rank = rank
        
和前面一样, init方法对每个属性定义一个可选形参. 默认的的卡牌是花草2.
要创建一个Card对象, 使用你想要的花色和大小调用Card:
queen_of_diaminds = Card(1, 13)

18.2 类属性
为了能将Card对象打印成人门容易约定的格式, 我们需要将整数编码映射成对应的大小和花色.
自然地做法是使用字符串列表. 我们将这些列表赋值到'类属性':
# 在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
        # 大小默认为2
        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)  # Jack of Hearts

18-1展示了Card类对象和一个Card实例.
Card是一个类对象, 所以它的类型是type. card1的类型是Card. 
为了节省空间, 我没有画出suit_names, rank_names的内容.

2023-04-22_00001

18.3 对比卡牌
对应内置类型, 我们比较操作符(<, >, ==)来比较对象并决定哪一个更大, 更小, 或者相等.
对应用户定义类型, 我们可以通过提供一个方法__lt__, 代码'less than'. 来重载内置操作符的行为.

__lt__接收两个形参, self和other, 当第一个对象严格小于第二个对象时返回True.

卡牌的正确顺序并不显而易见. 例如, 草花3和方块2哪个更大?
一个排面数大, 另一个花色大. 为了比较卡牌, 需要决定大小和花色哪个更重要.

这个问题的答案取决去你在玩哪种牌类游戏, 
但为了简单起见, 我们随意做一个决定, 认为花色更重要, 于是, 所有的黑桃比所有的方片都大, 依次类推.

这一点决定后, 我们就可以编写__lt__函数:
# 在Card类里:
	def __lt__(self, other):
		# 检查花色
        if self.suit < other.suit:
        	return True
        if self.suit > other.suit:
        	return False
			
        # 花色相同, 检查大小
        return self.rank < other.rank
    
使用元组比较, 可以写得更紧凑:
# 在Card类里:
	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')

    # 小于号 self是t1
    print(t1 < t2)
    # 大于号 self是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):
                # 一共抽 4 * 13 = 52张卡牌.
                card = Card(suit, rand)
                self.cards.append(card)
                
填充牌组最简单的办法是使用嵌套循环.
外层循环从03遍历各个花色. 内层循环从113遍历开牌大小.
每次迭代使用当前的花色和大小创建一个新的Card对象, 并将它添加到self.cards中.
18.5 打印牌组
下面是一个Deck的一个__str__方法:
# 在Deck类里:
	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
        # 大小默认为2
        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:
    # 初始化牌组对象, 按顺序创建52张卡牌.
    def __init__(self):
        self.cards = []
        # 花色
        for suit in range(4):
            # 大小
            for rand in range(1, 14):
                # 一共抽 4 * 13 = 52张卡牌.
                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为此提供了一个方便的功能:
# 在Deck类里
	def pop_card(self):
		return self.cards.pop()
		
由于pop从列表中抽出最后一张牌, 我们其实从牌组的低端发牌的.
要添加一个卡牌, 我们可以使用列表方法append:
# 在Deck类里:
	def add_card(self, card):
		self.cards.append(card)
        
像这样调用另一个方法, 却不做其他更多工作的方法, 有时候称为一个'饰面(veneer)'.
这个比喻来自于木工行业, 在木工行业饰面是为了改善外观而粘贴到便宜的木料表面的薄薄的一层优质木料.
(名字很高大尚, 里面没什么..)
在这个例子里, add_card是一个'薄薄'的方法, 用更适合牌组的术语来表达一个列表操作.
它改善了实现的外观(或接口).

作为另一个示例, 我们可以使用random模块的函数shuffle来编写一个Deck方法shuffle(洗牌):
# 在Deck类里
	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__方法来决定卡牌的顺序.
# 在Deck类里
    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类的方法:
# 在Head类里:
	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:
# 在Deck类里:
	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
        # 大小默认为2
        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):
                # 一共抽 4 * 13 = 52张卡牌.
                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')

    # 从牌组末尾发13张牌到手上.
    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之间的关系.

2023-04-24_00001

空心三角形箭头的线代表着一个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

# global variables
suffix_map = {}  # map from prefixes to a list of suffixes
prefix = ()  # current tuple of words


def process_file(filename, order=2):
    # 实例文件对象
    fp = open(filename)
    # 跳过开头
    skip_gutenberg_header(fp)

    # 跳过结尾
    for line in fp:
        if line.startswith('*** END OF THIS'):
            break

        # 将每一行的末尾\n去除, 再按空格切分得到单词列表. 最后遍历单词.
        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):
    # 声明prefix是全局的.
    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()))
    # 选出100个单词
    for i in range(n):
        # 获取这个键的后缀值, 如果没有这, 则返回None
        suffixes = suffix_map.get(start, None)
        # 后缀为None, 则从新执行random_text在选一个键. n的次数需要减去1.
        if suffixes is None:
            # 执行这句则不会执行return下面几行的代码
            random_text(n - i)
            return

        # 随机选择一个后缀
        word = random.choice(suffixes)
        # 打印后缀
        print(word, end=' ')
        # 重新设置前缀
        start = shift(start, word)


def shift(t, word):
    # 修改前缀, 前缀2改为前缀1, 单词改为前缀2
    return t[1:] + (word,)


# 参数1: 脚本路径, 参数2: 打开的文件名, 参数3: 生成单词文本的长度, 参数4: 前缀单词个数.
def main(script, filename='158-0.txt', n=100, order=2):
    try:
        # 如果用户提供的是字符串参数, 则可以使用int将纯字符串的数字转为整数.
        n = int(n)
        order = int(order)

    # 如果发生错误, 提示使用方法不正确.
    except ValueError:
        print('Usage: %d filename [# of words] [prefix length]' % script)

    # try正常结束执行下面的代码.
    else:
        # 参数1: 文件名, 前缀单词个数
        process_file(filename, order)
        # 随机生成文本
        random_text(n)


if __name__ == '__main__':
    # *sys.argv 获取文件的地址 C:\Backup\Program\ThinkPython\t1\t1.py
    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

            # 将每一行的末尾\n去除, 再按空格切分得到单词列表. 最后遍历单词.
            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()))
        # 选出100个单词
        for i in range(n):
            # 获取这个键的后缀值, 如果没有这, 则返回None
            suffixes = self.suffix_map.get(start, None)
            # 后缀为None, 则从新执行random_text在选一个键. n的次数需要减去1.
            if suffixes is None:
                # 执行这句则不会执行return下面几行的代码
                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:
        # 如果用户提供的是字符串参数, 则可以使用int将纯字符串的数字转为整数.
        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关系.

2023-04-25_00001

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
        # 大小默认为2
        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):
                # 一共抽 4 * 13 = 52张卡牌.
                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 = []
        # 对52张牌进行洗牌
        self.shuffle()

        # for循环创建多个手牌对象
        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):
    # 超出52张牌则提示输入错了:
    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"]

    # 初始化(默认卡牌为红色2)
    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])

    # 在两个对象进行 == 比较值的时候触发 __eq__() 的执行
    def __eq__(self, other):
        # 比较花色与大小
        return self.suit == other.suit and self.rank == other.rank

    # 在两个对象进行 < 小于比较值的时候触发 __lt__() 的执行
    def __lt__(self, other):
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return t1 < t2


# 牌组对象
class Deck:

    # 生成52张牌
    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()

    # 牌组发5张牌到手牌中
    deck.move_cards(hand, 5)
    # 排序
    hand.sort()
    # 查看手牌
    print(hand)

2. 如果你运行PokerHand.py, 它会连出7组包含7张卡片的扑克手牌, 并检查其中有没有顺子(因该是同花).
   在继续之前请仔细阅读代码.
# PokerHand.py
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():
            # 手牌中同一个花色有五张或大于五张则返回True.
            if val >= 5:
                return True
        return False


if __name__ == '__main__':
    # 实例牌组对象
    deck = Deck()
    # 洗牌
    deck.shuffle()

    # 循环实例7组手牌
    for i in range(7):
        #  PokerHand调用hand的初始化方法, 实例手牌对象.
        hand = PokerHand()
        # 牌组发7张牌到手牌中
        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():
            # 手牌中同一个花色有五张或大于五张则返回True.
            if val >= 5:
                return True
        return False

    # 对子
    def has_pair(self):
        self.rank_hist()
        # 取出直方图的值
        for val in self.ranks.values():
            # 手牌中同一个花色有五张或大于五张则返回True.
            if val >= 2:
                return True
        return False

    # 连对
    def has_twopair(self):
        # 对象计算
        pair_count = 0
        self.rank_hist()
        # 取出直方图的值
        for val in self.ranks.values():
            # 手牌中同一个花色有五张或大于五张则返回True.
            if val >= 2:
                pair_count += 1

        if pair_count >= 2:
            return True
        return False


if __name__ == '__main__':
    # 实例牌组对象
    deck = Deck()
    # 洗牌
    deck.shuffle()

    # 循环实例7组手牌
    for i in range(7):
        #  PokerHand调用hand的初始化方法, 实例手牌对象.
        hand = PokerHand()
        # 牌组发7张牌到手牌中
        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"]

    # 初始化(默认卡牌为红色2)
    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])

    # 在两个对象进行 == 比较值的时候触发 __eq__() 的执行
    def __eq__(self, other):
        # 比较花色与大小
        return self.suit == other.suit and self.rank == other.rank

    # 在两个对象进行 < 小于比较值的时候触发 __lt__() 的执行
    def __lt__(self, other):
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return t1 < t2


# 牌组对象
class Deck:

    # 生成52张牌
    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()

    # 牌组发5张牌到手牌中
    deck.move_cards(hand, 5)
    # 排序
    hand.sort()
    # 查看手牌
    print(hand)

from Card import Hand, Deck


class Hist(dict):
    """从每个项目(x)映射到其频率。"""

    def __init__(self, seq=[]):
        # 从sep遍历值, 并统计值出现的次数
        for x in seq:
            self.count(x)

    def count(self, x, f=1):
        # 设置属性值, 值为x出现的次数
        self[x] = self.get(x, 0) + f
        # 数值为0, 则删除属性
        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)

        # 将卡牌大小的统计值转为列表, 保存到sets属性中
        self.sets = list(self.ranks.values())
        # 降序
        self.sets.sort(reverse=True)

    # 是否为高牌
    def has_highcard(self):
        # 有一张牌即可
        return len(self.cards)

    def check_sets(self, *t):
        # *t收集所有的参数到元组中, self.sets 卡牌大小的统计值的倒序的列表. zip函数将两个序列合并成元组列表.
        for need, have in zip(t, self.sets):
            # 当提供的参数值 大于 类别中的值返回False, 否则返回True.
            if need > have:  # 这一句反正写看起来很变扭.
                return False
        return True

    # 是否有对子
    def has_pair(self):
        # zip((2, ), [x]) x的值 大于 提供的参数, 则说明有对子. 返回True.
        return self.check_sets(2)

    # 是否有两对
    def has_twopair(self):
        # zip((2, 2), [x1, x2]) x1与x2的 大于 提供的参数, 则说明有连个对子. 返回True.
        return self.check_sets(2, 2)

    # 三条
    def has_threekind(self):
        # zip((3, ), [x]) x的值 大于 提供的参数, 则说明有对子. 返回True.
        return self.check_sets(3)

    # 四条
    def has_fourkind(self):
        # zip((4, ), [x]) x的值 大于 提供的参数, 则说明有对子. 返回True.
        return self.check_sets(4)

    # 满堂红
    def has_fullhouse(self):
        # zip((3, 2), [x]) x的值 大于 提供的参数, 则说明有对子. 返回True.
        return self.check_sets(3, 2)

    # 同花
    def has_flush(self):
        # 获取花色直方图的值, 如果有一个值大于或等于5则返回True.
        for val in self.suits.values():
            if val >= 5:
                return True
        return False

    # 顺子
    def has_straight(self):
        # 复制一份花色直方图
        ranks = self.ranks.copy()
        # 添加项 键为14, 值为键x.
        ranks[14] = ranks.get(1, 0)

        # 顺子牌计数
        return self.in_a_row(ranks, 5)

    def in_a_row(self, ranks, n=5):
        # 计数器
        count = 0
        # 遍历值1-14
        for i in range(1, 15):
            # 从直方图中按1-14取值, 取到值则计算器加1
            if ranks.get(i, 0):
                count += 1
                # 连续取出5张牌都有值, 则是顺子返回True
                if count == n:
                    return True
            # 顺子中断, 计算器清零.
            else:
                count = 0
        # 没有顺子返回False
        return False

    # 同花顺
    def has_straightflush(self):

        # 集合(将手上的牌, 添加到集合中)
        s = set()
        # 遍历手牌上的牌
        for c in self.cards:
            # 按(大小, 花色) 添加到集合中
            s.add((c.rank, c.suit))
            # 卡牌为1, 集合添加(14, 花色)
            if c.rank == 1:
                s.add((14, c.suit))
            """
            对于牌面为A的牌, 因为A可以被视为1或14, 
            所以在集合s中需要同时添加(1, c.suit)和(14, c.suit)两个元素, 
            以便在检查同花顺时能够正确判断.
            """
        # (生成所有的牌组数字形式)
        # 遍历0 - 3
        for suit in range(4):
            count = 0
            # 遍历1-14
            for rank in range(1, 15):
                # 判断这张牌是否在集合中
                if (rank, suit) in s:
                    # 如果在则加1
                    count += 1
                    # 五张同花顺则返回True
                    if count == 5:
                        return True
                else:
                    count = 0
        return False

    # 同花顺-字典版本
    def has_straightflush(self):
        # 定义一个字典
        d = {}
        # 遍历卡牌
        for c in self.cards:
            # 往字典中保存项, (键为花色, 值为一个列表) PokerHand()得到一个扑克手对象,
            d.setdefault(c.suit, PokerHand()).add_card(c)  # add_card将卡牌添加到列表中.

        # 遍历字典的值, 值时一个列表
        for hand in d.values():
            # 花色列表的值小于5则跳过
            if len(hand.cards) < 5:
                continue
            # 5张卡片的花色相同, 使用直方图统计, 花色和大小.
            hand.make_histograms()
            # 判断是否有顺子, 如果有则返回True
            if hand.has_straight():
                return True
        return False

    def classify(self):
        # 对牌进行直方图统计
        self.make_histograms()
        # 设置对象的属性为列表(可以为一副牌打上多个标记)
        self.labels = []
        # 取出标记
        for label in PokerHand.all_labels:
            # 取出标记方法名(执行9个方法)
            f = getattr(self, 'has_' + label)
            # 执行方法名, 如果方法执行之后返回True则将标记添加到手牌属性的label列表中
            if f():
                self.labels.append(label)


class PokerDeck(Deck):

    def deal_hands(self, num_cards=5, num_hands=10):
        # 牌手列表
        hands = []
        # 生成7个实例
        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次,处理7的手,每7张牌
    n = 10000
    for i in range(n):
        # 打印循环的次数, 当i可以整除1000时打印i的值
        if i % 1000 == 0:
            print(i)

        # 生成牌组
        deck = PokerDeck()
        # 洗牌
        deck.shuffle()

        # 给7个人发7张牌
        hands = deck.deal_hands(7, 7)
        # 遍历7副牌
        for hand in hands:
            # 遍历牌组的标签
            for label in hand.labels:
                # 统计标签的数量
                lhist.count(label)

    # 70000 副牌的统计结果
    total = 7.0 * n
    print(total, 'hands dealt:')

    # 遍历所有标签
    for label in PokerHand.all_labels:
        # 依据标签获取属性, 属性记录这在牌组中出现的次数.
        freq = lhist.get(label, 0)
        # freq 的值为0, 跳过.
        if freq == 0:
            continue
        # 计算比较
        p = total / freq
        # 打印概率
        print('%s happens one time in %.2f' % (label, p))


if __name__ == '__main__':
    main()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/714285.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CSS期末复习速览(二)

1.元素显示模式分为三种&#xff1a;块元素&#xff0c;行内元素&#xff0c;行内块元素 2.块元素&#xff1a;常见的块元素&#xff1a;<h1>~<h6> <p> <div> <ul> <ol> <li>&#xff0c;特点&#xff1a;自己独占一行&a…

需求:如何给文件添加水印

今天给大家介绍一个简单易用的水印添加框架&#xff0c;框架抽象了各个文件类型的对于水印添加的方法。仅使用几行代码即可为不同类型的文件添加相同样式的水印。 如果你有给PDF、图片添加水印的需求&#xff0c;EasyWatermark是一个很好的选择&#xff0c;主要功能就是传入一…

嵌入式实训day5

1、 from machine import Pin import time # 定义按键引脚控制对象 key1 Pin(27,Pin.IN, Pin.PULL UP) key2 Pin(26,Pin.IN, Pin.PULL UP)led1 Pin(15,Pin.ouT, value0) led2 Pin(2,Pin.ouT, value0) led3 Pin(0,Pin.ouT, value0) # 定义key1按键中断处理函数 def key1 ir…

2.线上论坛项目

一、项目介绍 线上论坛 相关技术&#xff1a;SpringBootSpringMvcMybatisMysqlSwagger项目简介&#xff1a;本项目是一个功能丰富的线上论坛&#xff0c;用户可编辑、发布、删除帖子&#xff0c;并评论、点赞。帖子按版块分类&#xff0c;方便查找。同时&#xff0c;用户可以…

【CT】LeetCode手撕—121. 买卖股票的最佳时机

目录 题目1- 思路2- 实现⭐121. 买卖股票的最佳时机——题解思路 2- ACM实现 题目 原题连接&#xff1a;121. 买卖股票的最佳时机 1- 思路 模式识别 模式1&#xff1a;只能某一天买入 ——> 买卖一次 ——> dp 一次的最大利润 动规五部曲 1.定义dp数组&#xff0c;确…

跻身中国市场前三,联想服务器的“智变”与“质变”

IDC发布的《2024年第一季度中国x86服务器市场报告》显示&#xff0c;联想服务销售额同比增长200.2%&#xff0c;在前十厂商中同比增速第一&#xff0c;并跻身中国市场前三&#xff0c;迈入算力基础设施“第一阵营”。 十年砺剑联想梦&#xff0c;三甲登榜领风骚。探究联想服务器…

IDEA模版快速生成Java方法体

新建模版组myLive 在模版组下新建模版finit 在模版text内输入以下脚本 LOGGER.info("$className$.$methodName$>$parmas1$", $parmas2$); try {} catch (Exception e) {LOGGER.error("$className$.$methodName$>error:", e); }LOGGER.info("$c…

redis未授权到getshell

0 前言 现在是redis数据库未授权访问到getshell的部分了,不好意思&#xff0c;因为个人原因&#xff0c;和上篇mysql的getshell文章间隔较久. 1 漏洞产生原因 redis安装完之后&#xff0c;默认情况下绑定在 0.0.0.0:6379&#xff0c;且没有对登录IP做限制&#xff0c;并且没…

T113 Tina5.0 添加板级支持包

文章目录 环境介绍添加板级支持包修改板级文件验证总结 环境介绍 硬件&#xff1a;韦东山T113工业板 软件&#xff1a;全志Tina 5.0 添加板级支持包 进入源码目录<SDK>/device/config/chips/t113/configs&#xff0c;可以看到有如下文件夹&#xff1a; 复制一份evb1_…

python15 数据类型 集合类型

集合类型 无序的不重复元素序列 集合中只能存储不可变的数据类型 声明集合 使用 {} 定义 与列表&#xff0c;字典一样&#xff0c;都是可变数据类型 代码 集合类型 无序的不重复元素序列 集合中只能存储不可变的数据类型 声明集合 使用 大括号{} 定义 与列表&#xff0c;字典一…

linux驱动学习(十)之内存管理

一、linux内核启动过程中&#xff0c;关于内存信息 1、内核的内存的分区 [ 0.000000] Memory: 1024MB 1024MB total ---> 1G [ 0.000000] Memory: 810820k/810820k available, 237756k reserved, 272384K highmem [ 0.000000] Virtual kernel memory layout: 内…

UnityAPI学习之碰撞检测与触发检测

碰撞检测 发生碰撞检测的前提&#xff1a; 1. 碰撞的物体需要有Rigidbody组件和boxcllidder组件 2. 被碰撞的物体需要有boxcollider组件 示例1&#xff1a;被碰撞的物体拥有Rigidbody组件 两个物体会因为都具有刚体的组件而发生力的作用&#xff0c;如下图所示&#xff0c…

人工智能模型组合学习的理论和实验实践

组合学习&#xff0c;即掌握将基本概念结合起来构建更复杂概念的能力&#xff0c;对人类认知至关重要&#xff0c;特别是在人类语言理解和视觉感知方面。这一概念与在未观察到的情况下推广的能力紧密相关。尽管它在智能中扮演着核心角色&#xff0c;但缺乏系统化的理论及实验研…

Elasticsearch 8.1官网文档梳理 - 十一、Ingest pipelines(管道)

Ingest pipelines 管道&#xff08;Ingest pipelines&#xff09;可让让数据在写入前进行常见的转换。例如可以利用管道删除文档&#xff08;doc&#xff09;的字段、或从文本中提取数据、丰富文档&#xff08;doc&#xff09;的字段等其他操作。 管道&#xff08;Ingest pip…

【Mybatis-Plus】根据自定义注解实现自动加解密

背景 我们把数据存到数据库的时候&#xff0c;有些敏感字段是需要加密的&#xff0c;从数据库查出来再进行解密。如果存在多张表或者多个地方需要对部分字段进行加解密操作&#xff0c;每个地方都手写一次加解密的动作&#xff0c;显然不是最好的选择。如果我们使用的是Mybati…

Internet Download Manager(IDM6.41)软件安装包下载及安装教程

Internet Download Manager有一个智能下载逻辑加速器&#xff0c;具有智能动态文件分割和安全的多部分下载技术&#xff0c;可以加速下载。与其他下载加速器和管理器不同&#xff0c;Internet下载管理器在下载开始之前对文件进行分段&#xff0c;而Internet下载管理器在下载过程…

欧阳修,仕途波澜中的文坛巨匠

欧阳修&#xff0c;字永叔&#xff0c;号醉翁、六一居士&#xff0c;生于北宋真宗景德四年&#xff08;公元1007年&#xff09;&#xff0c;卒于北宋神宗熙宁五年&#xff08;公元1072年&#xff09;&#xff0c;享年65岁。他是北宋时期著名的文学家、史学家&#xff0c;也是唐…

SpringBoot 大文件基于md5实现分片上传、断点续传、秒传

SpringBoot 大文件基于md5实现分片上传、断点续传、秒传 SpringBoot 大文件基于md5实现分片上传、断点续传、秒传前言1. 基本概念1.1 分片上传1.2 断点续传1.3 秒传1.4 分片上传的实现 2. 分片上传前端实现2.1 什么是WebUploader&#xff1f;功能特点接口说明事件APIHook 机制 …

索引失效有效的11种情况

1全职匹配我最爱 是指 where 条件里 都是 &#xff0c;不是范围&#xff08;比如&#xff1e;,&#xff1c;&#xff09;&#xff0c;不是 不等于&#xff0c;不是 is not null&#xff0c;然后 这几个字段 建立了联合索引 &#xff0c;而且符合最左原则。 那么就要比 只建…

[C++] vector list 等容器的迭代器失效问题

标题&#xff1a;[C] 容器的迭代器失效问题 水墨不写bug 正文开始&#xff1a; 什么是迭代器&#xff1f; 迭代器是STL提供的六大组件之一&#xff0c;它允许我们访问容器&#xff08;如vector、list、set等&#xff09;中的元素&#xff0c;同时提供一个遍历容器的方法。然而…