View Categories

Python 教程 18 — 继承

22 min read

最常与面向对象编程联系在一起的语言特性就是 继承 。继承指的是在现有类的基础下进行修改,从而定义新类的能力。在本章中,我会用表示卡牌(playing cards)、一副牌(deck of hands)和牌型(poker hands)的类,来展示继承这一特性。

"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import random


class Card:
    """Represents a standard playing card.
    
    Attributes:
      suit: integer 0-3
      rank: integer 1-13
    """

    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):
        """Returns a human-readable string representation."""
        return '%s of %s' % (Card.rank_names[self.rank],
                             Card.suit_names[self.suit])

    def __eq__(self, other):
        """Checks whether self and other have the same rank and suit.
        returns: boolean
        """
        return self.suit == other.suit and self.rank == other.rank

    def __lt__(self, other):
        """Compares this card to other, first by suit, then rank.
        returns: boolean
        """
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return t1 < t2


class Deck:
    """Represents a deck of cards.
    Attributes:
      cards: list of Card objects.
    """
    
    def __init__(self):
        """Initializes the Deck with 52 cards.
        """
        self.cards = []
        for suit in range(4):
            for rank in range(1, 14):
                card = Card(suit, rank)
                self.cards.append(card)

    def __str__(self):
        """Returns a string representation of the deck.
        """
        res = []
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)

    def add_card(self, card):
        """Adds a card to the deck.
        card: Card
        """
        self.cards.append(card)

    def remove_card(self, card):
        """Removes a card from the deck or raises exception if it is not there.
        
        card: Card
        """
        self.cards.remove(card)

    def pop_card(self, i=-1):
        """Removes and returns a card from the deck.
        i: index of the card to pop; by default, pops the last card.
        """
        return self.cards.pop(i)

    def shuffle(self):
        """Shuffles the cards in this deck."""
        random.shuffle(self.cards)

    def sort(self):
        """Sorts the cards in ascending order."""
        self.cards.sort()

    def move_cards(self, hand, num):
        """Moves the given number of cards from the deck into the Hand.
        hand: destination Hand object
        num: integer number of cards to move
        """
        for i in range(num):
            hand.add_card(self.pop_card())


class Hand(Deck):
    """Represents a hand of playing cards."""
    
    def __init__(self, label=''):
        self.cards = []
        self.label = label


def find_defining_class(obj, method_name):
    """Finds and returns the class object that will provide 
    the definition of method_name (as a string) if it is
    invoked on obj.
    obj: any python object
    method_name: string method name
    """
    for ty in type(obj).mro():
        if method_name in ty.__dict__:
            return ty
    return None


if __name__ == '__main__':
    deck = Deck()
    deck.shuffle()

    hand = Hand()
    print(find_defining_class(hand, 'shuffle'))

    deck.move_cards(hand, 5)
    hand.sort()
    print(hand)

1.卡牌对象 #

一副牌有52张牌,每一张属于4种花色的一个和13个等级的一个。 4种花色是黑桃(Spades),红心(Hearts),方块(Diamonds),梅花(Clubs), 以桥牌中的逆序排列。13个等级是A、2、3、4、5、6、7、8、9、10、J、Q、K。 根据你玩的游戏的不同,A 可能比 K 大或者比 2 小。

如果我们定义一个新的对象来表示卡牌,明显它应该有rank(等级) 和suit(花色) 两个属性。但两个属性的类型不太明显。一个可能是使用字符串类型, 如'Spade'表示花色,'Queen'表示等级。这种实现的一个问题是,不是那么容易比较牌的大小,看哪张牌的等级或花色更高。

另外一种方法,是使用一个整型来 编码 等级和花色。 在这里,“编码”表示我们要定义一个数字到花色或数字到等级的映射。 但是这里的编码并不是为了保密(那就成了“加密”)。

例如,下面的表格列出了花色和对应的整数码:

Spades↦↦3
Hearts↦↦2
Diamonds↦↦1
Clubs↦↦0

整数码使得很容易比较牌的大小;因为更高的花色对应更高的数字,我们可以通过比较数字,来判断花色的的大小。

等级的映射类型选择就显而易见;每个数字等级对应相应的整数,然后对于J,K,Q:

Jack↦↦11
Queen↦↦12
King↦↦13

这里,我使用↦↦符号来清楚的表示,这些不是 Python 程序的一部分。它们属于程序设计的一部分,但是不会出现在代码中。

Card的类定义如下:

class Card:
    """代表一张标准的卡牌"""

    def __init__(self, suit=0, rank=2):
        self.suit = suit
        self.rank = rank

通常,init 方法接受针对每个属性的可选形参。默认的卡牌是梅花 2。

可以使用你需要的花色和等级调用 Card ,创建一个 Card 对象。

queen_of_diamonds = Card(1, 12)

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 是一个卡牌对 象,self.rank 是它的等级。 同样的,Card 是一个类对象,Card.rank_names是一个和类关联的字符串列表。

每一张卡牌都有自己的花色和等级, 但是这里只有一份suit_namesrank_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

图18-1:对象图

图18-1:对象图是 Card 类对象和一个 Card 实例的图示。Card 是一个类对象;它的类型是 type 。card1 是 Card 的一个实例,因此它的类型是 Card 。为了节省空间,我没有画出suit_names 和 rank_names的内容。

3.比较卡牌 #

对于内建类型,有关系运算符(<, >, ==, 等等)可以比较值,判断哪一个是大于、小于或等于另外一个。 对于程序员自定义的类型,我们可以通过提供一个叫 __lt__(代表“小于”)的方法,来覆盖内建运算符的行为。

__lt__接受 2 个参数, self 和 other,如果 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

我们做个练习,编写一个 Time 对象的 __lt__ 方法。你可以使用元组比较,也可以考虑比较整数。

4.一副牌 #

现在我们有 Card 类了,下一步是定义完整的一副牌(Deck)了。因为一副牌由许多牌组成,自然地 每一个 Deck 都有一个卡牌列表作为属性。

下面是一个 Deck 的类定义。初始化方法创建了 cards 属性,然后生成了由52张牌组成一副标准卡牌。

class Deck:

    def __init__(self):
        self.cards = []
        for suit in range(4):
            for rank in range(1, 14):
                card = Card(suit, rank)
                self.cards.append(card)

生成一副牌的最简单方法是使用嵌套循环。外层循环枚举 0 到 3 的花色。内层循环枚举 1 到 13 的等级。每一个迭代都用当前的花色和等级创建一张新的牌。然后放入 self.cards 中。

5.打印一副牌 #

下面是为 Deck 定义的 __str__ 方法:

# Deck类的内部

    def __str__(self):
        res = []
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)

这个方法展示了累积大字符串的高效方法:建立一个字符串列表然后使用字符串方法 join 。 内建函数 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行,但他实际上是包含换行符的一个长字符串。

6.添加,移除,洗牌和排序 #

为了发牌,我们需要一个可以把卡牌从一副牌中移除并返回的方法。 列表的 pop 方法提供了一个便捷的实现:

# Deck类的内部

    def pop_card(self):
        return self.cards.pop()

Since pop removes the last card in the list, we are dealing from the bottom of the deck.

由于 pop 移除列表的 最后一张 卡牌,所以我们从牌底开始发牌。

我们可以使用列表的 append 方法,添加一张卡牌:

# Deck类的内部

    def add_card(self, card):
        self.cards.append(card)

像上面这样利用别的方法(method),自己却没有做太多处理的方法,有时候被称为 伪装方法(veneer) 。 这个隐喻来源于木工行业,他们通常用一片高质量的木质薄层粘贴在一块便宜木材的表面,改善外观形象。

在这里,add_card 是一个“瘦”方法,以卡牌的术语来表述一个列表操作。它改善了实现的外观,或者说接口。

再举一个例子,我们可以用 random 模块中的 shuffle 函数,给 Deck 写一个叫 shuffle 的方法。

# Deck类的内部

    def shuffle(self):
        random.shuffle(self.cards)

不要忘记了导入 random 。

我们做个练习,用列表的 sort 方法来写一个 Deck 的 sort 方法,给卡牌排序。 sort使用我们定义的__cmp__来决定排序顺序。

7.继承 #

继承指的是在现有类的基础下进行修改,从而定义新类的能力。例如,假设我们想定义一个类来代表手牌(hand),即玩家目前手里有的牌。手牌与一副牌(deck)类似:二者都由卡牌组成,都要求支持添加和移除卡牌的操作。

但二者也有区别;有些我们希望手牌具备的操作,对于 deck 来说并不合理。例如,在扑克牌中,我们可能需要比较两个手牌,比较哪方赢了。在桥牌中,我们可能需要计算手牌的得分,才好下注。

类之间有相似之处,但也存在不同,这时就可以用上继承了。你只需要在定义新类时,将现有类的名称放在括号里,即可继承现有类:

class Hand(Deck):
    """Represents a hand of playing cards."""

这个定义表明,Hand 继承自 Deck ;这意味着我们也可以对 Hands 使用 Deck 的pop_cardadd_card方法。

当一个新类继承自一个现有类时,现有类被称为 父类 ,新类被称为 子类 。

在此例中,Hand 继承了 Deck 的__init__方法,但是它并没有满足我们的要求:init 方法应该为 Hand 初始化一个空的 cards 列表,而不是往手牌里添加 52 张新牌。

如果我们提供一个 Hand 的 init 方法,它会覆盖从 Deck 类继承来的同名方法。

# Hand 类的内部

    def __init__(self, label=''):
        self.cards = []
        self.label = label

当你创建一个 Hand 时,Python 会调用这个 init 方法,而不是 Deck 中的同名方法。

>>> hand = Hand('new hand')
>>> hand.cards
[]
>>> hand.label
'new hand'

其它方法是从 Deck 继承来的,所以我们可以使用pop_card 和 add_card来发牌:

>>> 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())

move_cards接受两个参数,一个是 Hand 对象,另一个是发牌的数量。 它会同时修改 self 和 hand ,然后返回 None 。

在有些游戏里面,卡牌从一个手牌移动到另外一个手牌,或者从手牌退还到牌堆里面。 任何这些操作都可以使用 move_cardsself 可以是一个 Deck 或者一个 Hand ,而且尽管名字叫 hand ,它也可以是一个 Deck 。

继承是一个非常有用的特性。有了继承,一些重复性的代码可以写得非常的优雅。 继承有助于代码重用,因为你可以在不修改父类定义的前提下,就改变父类的行为。 在有些情况下,继承的结构反映了真实问题的结构,使得程序更易于理解。

另一方面,继承又有可能会使得程序更加难读。 当调用一个方法时,有时候搞不清楚去哪找它的定义。 相关的代码可能被分散在几个模块之中。 而且,许多用继承能完成的事情,不用继承也可以完成,有可能还完成得更好。

8.类图 #

到目前为止我们已经了解过栈图,它显示的是一个程序的状态;以及对象图,它显示的是一个对象的属性及其值。这些图代表了程序执行中的一个快照,所以它们随着程序的运行而变化。

它们也十分的详细;但有些时候显得过于详细了。类图是程序结构的一种更加抽象的表达。 它显示的是类和类之间的关系,而不是每个独立的对象。

类之间有如下几种关系:

  • 一个类中的对象可以包含对另外一个类的对象的引用。例如,每一个矩形包含对点的引用,每一个 Deck 包含对许多 Card 的引用。这种关系被称为组合( HAS-A ),可以类似这样描述:“一个矩形有一个(has a)点”。
  • 一个类可能继承自另外一个类。这种关系被称为继承(IS-A),可以类似这样描述:“Hand is a kind of Deck”。
  • 一个类可能强赖另一个类,因为前者中的对象接受后者中的对象作为参数,或者使用后者中的对象作为计算的一部分。这种关系被称为 依赖 。

类图是这些关系的图形化表示。例如,图18-2:类图标明了 Card , Deck 和 Hand 之间的关系。

图18-2:类图

带空心三角的箭头表示 IS-A 的关系;这里它表示 Hand 继承自 Deck 。

标准箭头表示 HAS-A 的关系;这里表示 Deck 包含对 Card 对象的引用。

箭头旁边的星号是一个复数( multiplicity )表达;它表示 Deck 包含多少个 Card 。一个复数表达可以是一个简单的数字(如 52 ),一个范围(如5..7)或者是*,表示有任意数量的 Card 。

上图中没有标出依赖关系。这种关系通常使用虚线箭头表示。或者,如果有很多依赖关系的话,有时候会省略。

一个更详细的类图可能会显示 Deck 实际包含了一个由 Cards 组成的列表,但是通常类图中不会包含 list 和 dict 等内建类型。

9.数据封装 #

前面几章中描述了一种可以称为”面向对象设计“的开发计划。我们确定所需要的对象 —— 如“Point“ 、 Rectangle 和 Time —— 然后定义代表它们的类。 对于每个类来说,这个类对象和真实世界(或至少是数学世界)中的某种实体具有明显的对应关系。

但是有时有很难界定你需要的对象以及它们如何交互。在这个时候,你需要一个不同的开发计划。之前我们通过封装和泛化来编写函数接口,我们同样可以通过 数据封装 来编写类接口。

马尔科夫分析一节中介绍的马尔科夫分析就是一个很好的例子。如果你仔细阅读了文章的代码,你会发现它使用了两个全局变量 —— suffix_mapprefix,它们被多个函数进行读写。

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 KeyError:
        # if there is no entry for this prefix, make one
        self.suffix_map[self.prefix] = [word]

    self.prefix = shift(self.prefix, word)

像这样改变一个程序 —— 改变设计而保持功能不变 —— 是代码重构的另一个例子(参见重构一节)。

下面的例子给出了一种设计对象和方法的开发计划:

  1. 首先编写读取全局变量的函数(如有必要)。
  2. 一旦你让程序跑起来了,开始查找全局变量和使用它们的函数的联系。
  3. 封装相关的变量作为一个对象的属性。
  4. 转换相关函数为新类的方法。

我们做个练习,从下方获取我的马尔科夫分析代码:

"""This module contains a code example related to

Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com

Copyright 2015 Allen Downey

License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import sys
import string
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):
    """Reads a file and performs Markov analysis.

    filename: string
    order: integer number of words in the prefix

    returns: map from prefix to list of possible suffixes.
    """
    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):
    """Reads from fp until it finds the line that ends the header.

    fp: open file object
    """
    for line in fp:
        if line.startswith('*** START OF THIS'):
            break


def process_word(word, order=2):
    """Processes each word.

    word: string
    order: integer

    During the first few iterations, all we do is store up the words; 
    after that we start adding entries to the dictionary.
    """
    global prefix
    if len(prefix) < order:
        prefix += (word,)
        return

    try:
        suffix_map[prefix].append(word)
    except KeyError:
        # if there is no entry for this prefix, make one
        suffix_map[prefix] = [word]

    prefix = shift(prefix, word)


def random_text(n=100):
    """Generates random wordsfrom the analyzed text.

    Starts with a random prefix from the dictionary.

    n: number of words to generate
    """
    # choose a random prefix (not weighted by frequency)
    start = random.choice(list(suffix_map.keys()))
    
    for i in range(n):
        suffixes = suffix_map.get(start, None)
        if suffixes == None:
            # if the start isn't in map, we got to the end of the
            # original text, so we have to start again.
            random_text(n-i)
            return

        # choose a random suffix
        word = random.choice(suffixes)
        print(word, end=' ')
        start = shift(start, word)


def shift(t, word):
    """Forms a new tuple by removing the head and adding word to the tail.

    t: tuple of strings
    word: string

    Returns: tuple of strings
    """
    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)
        print()


if __name__ == '__main__':
    main(*sys.argv)

然后按照上面所述的步骤,将全局变量封装为新类 Markov (注意M为大写)的属性。

10.调试 #

继承会使得调试变得更加复杂,因为你可能不知道实际调用的是哪个类的方法。

假设你在写一个处理 Hand 对象的函数。你可能会想让它可以处理所有种类的 Hand ,如 PockerHands ,BridgeHands ,等等。如果你调用类似 shuffle 这样的方法,你可能会得到 Deck 中定义的那个, 但是如果有任何一个子类覆盖了这个方法。你实际上得到的是子类的那个方法。这个行为通常是一件好事,但是容易让人混淆。

只要你不确定程序的执行流程,最简单的方法是在相关方法的开始处添加 print 语 句。如果 Deck.shuffle 打印一条如像 Running Deck.shuffle 的消息,那么随着程序的运行,它会追踪执行的流程。

另外一种方法是使用下面的函数,它接受一个对象和一个方法的名字(字符串格式)作 为参数,然后返回提供这个方法定义的类:

def find_defining_class(obj, meth_name):
    for ty in type(obj).mro():
        if meth_name in ty.__dict__:
            return ty

例如:

>>> hand = Hand()
>>> find_defining_class(hand, 'shuffle')
<class 'Card.Deck'>

所以 Hand 的 shuffle 方法是来自于 Deck 的。

find_defining_class使用 mro 方法获得将类对象(类型)的列表, 解释器将会从这里依次搜索哪个类提供了这个方法。“MOR”是“method resolution order”的简称,指的是Python “解析” 方法名时将搜索的一个类序列。

我提一个对程序设计的建议:当你覆盖一个方法时,新方法的接口应该与旧方法保持一致。 它们应该接受相同的参数,返回相同的类型,遵守相同的先决条件和后置条件。 如果你遵循这个原则,你会发现:任何你设计的函数,只要能用于一个父类的对象( 如 Deck ),就能够用于任何子类的实例(如 Hand 和 PockerHand )。

如果你违背这条规则(该原则被称为“里氏代换原理”,英文为:Liskov substitution principle),你的代码逻辑就会变得乱七八糟。

11.术语表 #

编码(encode):利用另一组值代表一组值,方法时构建二者之间的映射。

类属性(class attribute):与类对象相关联的属性。类属性定义在类定义的内部,但在方法的外部。

实例属性(instance attribute):与类的实例相关联的属性。

伪装方法(veneer):提供另一个函数的不同接口,但不做太多计算的函数或方法。

继承(inheritance):在此前定义的类的基础下进行修改,从而定义一个新类的能力。

父类(parent class):子类所继承自的类。

子类(child class):通过继承一个现有类创建的新类。

IS-A 关系:子类和父类之间的关系。

HAS-A 关系:两个类之中,有一个类包含对另一个类的实例的引用的关系。

依赖(dependency):两个类之中,一个类的实例使用了另一个类的实例,但没有将其保存为属性的关系。

类图(class diagram):表明程序中包含的类及其之间的关系的图示。

复数(multiplicity):类图中的一种标记,表明在 HAS-A 关系中,某个对包含了多少个对另一个类实例的引用。

数据封装(data encapsulation):一种程序开发计划,包括首先编写一个使用全局变量的原型,然后再讲全局变量变成实例属性的最终版代码。

12.练习题 #

习题18-1 #

针对以下程序,画一个 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)

习题18-2 #

为 Deck 编写一个叫 deal_hands 的方法,接受两个参数:手牌的数量以及每个手牌的卡牌数。它应该创建相应数量的 Hand 对象,给每个手牌发放相应数量的卡牌,然后返回一个 Hands 列表。

下面是扑克牌中可能的手牌(牌型),越往下值越大,几率越低:

对牌:两张相同牌面的牌

两对牌:两对相同牌面的牌

三条:三张等级相同的牌

顺子:五张连续的牌(A可高可低。如A-2-3-4-5是一个顺子,10-J-Q-K-A也 是。但是Q-K-A-2-3就不是)

同花:五张花色一样的牌

三代二:三张等级一样的牌,另外两张等级一样的牌

四条:四张牌面一样的牌

同花顺:五张花色相同的等级连续的牌

习题18-3 #

下面这些习题的目的,是估算抽到不同手牌的几率。

  1. 从下方折叠组件中获取以下文件:Card.py: 本章中完整版本的Card , Deck和Hand类。PokerHand.py: 代表 poker hand 的不完整的实现,和一些测试代码。
  2. 如果你运行 PokerHand.py ,它会发放 7 张牌的 poker hand,检查是否含有顺子。仔细阅读代码,再继续下面的内容。
  3. 往 PokerHand.py 文件中添加叫做 has_pair 、 has_twopair 等方法,这些方法根据手牌是否满足相应的标准来返回 True 或 False 。你的代码应该可以正确地处理包含任意卡牌数量(虽然 5 和 7 是最常见的数量)的手牌。
  4. 写一个叫 classify 的方法,计算出一个手牌的最高值分类,然后设置对应的 label 属性。例如,一个 7 张牌的手牌可能包含一个顺子和一个对子;那么它应该标注为“顺子”。
  5. 确信你的分类方法是正确的之后,下一步是估算这些不同手牌出现的几率。在 PokerHand.py 中编写一个函数,完成洗牌,分牌,对牌分类,然后记录每种分类出现的次数。
  6. 打印每种分类和对应频率的表格。运行你的程序,不断增加手牌的卡牌数量,直到输出的值保持在足够准确的范围。将你的结果和http://en.wikipedia.org/wiki/Hand_rankings 页面中的的值进行比较。
"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import random


class Card:
    """Represents a standard playing card.
    
    Attributes:
      suit: integer 0-3
      rank: integer 1-13
    """

    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):
        """Returns a human-readable string representation."""
        return '%s of %s' % (Card.rank_names[self.rank],
                             Card.suit_names[self.suit])

    def __eq__(self, other):
        """Checks whether self and other have the same rank and suit.
        returns: boolean
        """
        return self.suit == other.suit and self.rank == other.rank

    def __lt__(self, other):
        """Compares this card to other, first by suit, then rank.
        returns: boolean
        """
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return t1 < t2


class Deck:
    """Represents a deck of cards.
    Attributes:
      cards: list of Card objects.
    """
    
    def __init__(self):
        """Initializes the Deck with 52 cards.
        """
        self.cards = []
        for suit in range(4):
            for rank in range(1, 14):
                card = Card(suit, rank)
                self.cards.append(card)

    def __str__(self):
        """Returns a string representation of the deck.
        """
        res = []
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)

    def add_card(self, card):
        """Adds a card to the deck.
        card: Card
        """
        self.cards.append(card)

    def remove_card(self, card):
        """Removes a card from the deck or raises exception if it is not there.
        
        card: Card
        """
        self.cards.remove(card)

    def pop_card(self, i=-1):
        """Removes and returns a card from the deck.
        i: index of the card to pop; by default, pops the last card.
        """
        return self.cards.pop(i)

    def shuffle(self):
        """Shuffles the cards in this deck."""
        random.shuffle(self.cards)

    def sort(self):
        """Sorts the cards in ascending order."""
        self.cards.sort()

    def move_cards(self, hand, num):
        """Moves the given number of cards from the deck into the Hand.
        hand: destination Hand object
        num: integer number of cards to move
        """
        for i in range(num):
            hand.add_card(self.pop_card())


class Hand(Deck):
    """Represents a hand of playing cards."""
    
    def __init__(self, label=''):
        self.cards = []
        self.label = label


def find_defining_class(obj, method_name):
    """Finds and returns the class object that will provide 
    the definition of method_name (as a string) if it is
    invoked on obj.
    obj: any python object
    method_name: string method name
    """
    for ty in type(obj).mro():
        if method_name in ty.__dict__:
            return ty
    return None


if __name__ == '__main__':
    deck = Deck()
    deck.shuffle()

    hand = Hand()
    print(find_defining_class(hand, 'shuffle'))

    deck.move_cards(hand, 5)
    hand.sort()
    print(hand)
"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

from Card import Hand, Deck


class PokerHand(Hand):
    """Represents a poker hand."""

    def suit_hist(self):
        """Builds a histogram of the suits that appear in the hand.
        Stores the result in attribute suits.
        """
        self.suits = {}
        for card in self.cards:
            self.suits[card.suit] = self.suits.get(card.suit, 0) + 1

    def has_flush(self):
        """Returns True if the hand has a flush, False otherwise.
      
        Note that this works correctly for hands with more than 5 cards.
        """
        self.suit_hist()
        for val in self.suits.values():
            if val >= 5:
                return True
        return False


if __name__ == '__main__':
    # make a deck
    deck = Deck()
    deck.shuffle()

    # deal the cards and classify the hands
    for i in range(7):
        hand = PokerHand()
        deck.move_cards(hand, 7)
        hand.sort()
        print(hand)
        print(hand.has_flush())
        print('')
"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

from Card import Hand, Deck


class Hist(dict):
    """A map from each item (x) to its frequency."""

    def __init__(self, seq=[]):
        "Creates a new histogram starting with the items in seq."
        for x in seq:
            self.count(x)

    def count(self, x, f=1):
        "Increments (or decrements) the counter associated with item x."
        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):
        """Computes histograms for suits and hands.
        Creates attributes:
          suits: a histogram of the suits in the hand.
          ranks: a histogram of the ranks.
          sets: a sorted list of the rank sets in the hand.
        """
        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):
        """Returns True if this hand has a high card."""
        return len(self.cards)
        
    def check_sets(self, *t):
        """Checks whether self.sets contains sets that are
        at least as big as the requirements in t.
        t: list of int
        """
        for need, have in zip(t, self.sets):
            if need > have:
                return False
        return True

    def has_pair(self):
        """Checks whether this hand has a pair."""
        return self.check_sets(2)
        
    def has_twopair(self):
        """Checks whether this hand has two pair."""
        return self.check_sets(2, 2)
        
    def has_threekind(self):
        """Checks whether this hand has three of a kind."""
        return self.check_sets(3)
        
    def has_fourkind(self):
        """Checks whether this hand has four of a kind."""
        return self.check_sets(4)

    def has_fullhouse(self):
        """Checks whether this hand has a full house."""
        return self.check_sets(3, 2)

    def has_flush(self):
        """Checks whether this hand has a flush."""
        for val in self.suits.values():
            if val >= 5:
                return True
        return False

    def has_straight(self):
        """Checks whether this hand has a straight."""
        # make a copy of the rank histogram before we mess with it
        ranks = self.ranks.copy()
        ranks[14] = ranks.get(1, 0)

        # see if we have 5 in a row
        return self.in_a_row(ranks, 5)

    def in_a_row(self, ranks, n=5):
        """Checks whether the histogram has n ranks in a row.
        hist: map from rank to frequency
        n: number we need to get to
        """
        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):
        """Checks whether this hand has a straight flush.
        Clumsy algorithm.
        """
        # make a set of the (rank, suit) pairs we have
        s = set()
        for c in self.cards:
            s.add((c.rank, c.suit))
            if c.rank == 1:
                s.add((14, c.suit))

        # iterate through the suits and ranks and see if we
        # get to 5 in a row
        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):
        """Checks whether this hand has a straight flush.
        Better algorithm (in the sense of being more demonstrably
        correct).
        """
        # partition the hand by suit and check each
        # sub-hand for a straight
        d = {}
        for c in self.cards:
            d.setdefault(c.suit, PokerHand()).add_card(c)

        # see if any of the partitioned hands has a straight
        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):
        """Classifies this hand.
        Creates attributes:
          labels:
        """
        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):
    """Represents a deck of cards that can deal poker hands."""

    def deal_hands(self, num_cards=5, num_hands=10):
        """Deals hands from the deck and returns Hands.
        num_cards: cards per hand
        num_hands: number of hands
        returns: list of Hands
        """
        hands = []
        for i in range(num_hands):        
            hand = PokerHand()
            self.move_cards(hand, num_cards)
            hand.classify()
            hands.append(hand)
        return hands


def main():
    # the label histogram: map from label to number of occurances
    lhist = Hist()

    # loop n times, dealing 7 hands per iteration, 7 cards each
    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)
            
    # print the results
    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()

贡献者 #

  1. 翻译:@bingjin
  2. 校对:@bingjin
  3. 参考:@carfly

推荐阅读 #

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。


​Python实用宝典 (pythondict.com)
关注公众号:Python实用宝典
更多精彩文章等你阅读

Powered by BetterDocs

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

退出移动版
微信支付
请使用 微信 扫码支付