【练习题】第十八章--继承(Think Python)

一个带有映射性质的类:

class Card:
    def __init__(self, suit=0, rank=2):
        ''' 类的实例属性'''
        self.suit = suit
        self.rank = rank
    '''类的属性'''
    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])
    def __lt__(self, other):
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return t1 < t2

 定义成副纸牌:

class Deck:
    def __init__(self):
        self.cards = []
        for suit in range(4):
            for rank in range(1, 14):
                card = Card(suit, rank)
                self.cards.append(card)
    def __str__(self):
        res = []
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)
#抽牌
    def pop_card(self):
        return self.cards.pop()
#加牌
    def add_card(self, card):
        self.cards.append(card)
#洗牌
    def shuffle(self):
        random.shuffle(self.cards)

继承

继承就是基于已有的类进行修改来获取新类的能力。举个例子,比方说我们需要一个表示『一手牌』的类,这个就是指一个牌手手中拿着的牌。『一手牌』和『一副牌』有些相似:都是由一系列的纸牌组成的,也都要有添加和移除纸牌的运算。

『一手牌』还和『一副牌』有所区别;对于手中的牌有一些运算并不适用于整副的牌。比如说,在扑克游戏中,我们可能需要对比两手牌来看看哪一副胜利。在桥牌里面,还可能需要对手中的牌进行计分以决胜负。

类之间这种相似又有区别的关系,就适合用继承来实现了。要继承一个已有的类来定义新类,就要把已有类的名字放到括号中,如下所示:

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

上面这样的定义就表示了 Hand 继承了 Deck;也就意味着我们可以在 Hands 中使用 Decks 中的那些方法,比如 pop_card 以及 add_card 等等。

当一个新类继承了一个已有的类时,这个已有的类就叫做基类,新定义的类叫做子类。

在本章的这个例子中,Hand 类从 Deck 类继承了init方法,但这个方法和我们的需求还不一样:Hand类的 init 方法应该用一个空列表来初始化手中的牌,而不是像 Deck 类中那样用一整副52张牌。

# inside class 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 的方法放进去:

#inside class Deck:
def move_cards(self, hand, num):
    for i in range(num):
        hand.add_card(self.pop_card())

move_cards 方法接收两个参数,一个 Hand 对象,以及一个要处理的纸牌数量。该方法会修改 self 和 hand。返回为空。

在有的游戏中,纸牌需要从一手牌拿出去放到另外一手牌中去,或者从手中拿出去放到牌堆里面。这就亏用 move_cards 来实现这些操作:第一个变量 self 可以是一副牌也可以是一手牌,第二个变量虽然名字是 hand,实际上也可以是一个 Deck 对象。

继承是一个很有用的功能。有的程序如果不用继承的话就会有很多重复代码,用继承来写出来就会更简洁很多了。继承有助于代码重用,因为你可以对基类的行为进行定制而不用去修改基类本身。在某些情况下,继承的结构也反映了要解决的问题中的自然关系,这就让程序设计更易于理解。

然而继承也容易降低程序可读性。当调用一个方法的时候,有时候不容易找到该方法的定义位置。相关的代码可能跨了好几个模块。此外,很多事情可以用继承来实现,但不用继承也能做到同样效果,甚至做得更好。

调试

该函数接收一个对象和一个方法的名字(作为字符串),然后返回提供该方法定义的类的名称。

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 方法来获取所有搜索方法的类对象的列表。 『MRO』的意思是『method resolution order(方法 解决方案 顺序)』,也就是 Python 搜索来找到方法名的类的序列。

下面是一个在设计上的建议:当你覆盖一个方法的时候,新的方法的接口最好同旧的完全一致。应该接收同样的参数,返回同样类型,并且遵循同样的前置条件和后置条件。只要你遵守这个规则,你就会发现所有之前设计来处理一个基类的函数,比如处理 Deck 的,就都可以用于子类的实例上面,比如 Hand 类或者 PokerHand 类。

如果你违背了上面这个『里氏替换原则』,你的代码就可能很悲剧地崩溃,就像无数纸牌坍塌一样。

数据封装

一种设计累的对象和方法的开发规划模式:

  1. 先开始写一些函数来读去和写入全局变量(在必要的情况下)。

  2. 一旦程序可以工作了,就检查一下全局变量与使用它们的函数之间的关系。

  3. 把相关的变量作为类的属性封装到一起。

  4. 把相关的函数转换成新类的方法。

猜你喜欢

转载自blog.csdn.net/qq_29567851/article/details/83793812