Python 教程 18 — 继承

最常与面向对象编程联系在一起的语言特性就是 继承 。继承指的是在现有类的基础下进行修改,从而定义新类的能力。在本章中,我会用表示卡牌(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:对象图

图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:类图

图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

推荐阅读 #

推荐一个非常优惠的QMT开户渠道 #
很多喜欢玩量化的同学都想要找一个靠谱且低费率能做自动化的券商。 我之前也推荐过一个渠道,但是因为他们公司内部问题,之前的那个开户渠道也遗憾下线了,今天给大家找到了一个新的渠道,费率如下: 有需要的可以直接联系我的微信: 83493903 (

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


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

Powered by BetterDocs

评论(0)

提示:请文明发言

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