一个带有映射性质的类:
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 类。
如果你违背了上面这个『里氏替换原则』,你的代码就可能很悲剧地崩溃,就像无数纸牌坍塌一样。
数据封装
一种设计累的对象和方法的开发规划模式:
-
先开始写一些函数来读去和写入全局变量(在必要的情况下)。
-
一旦程序可以工作了,就检查一下全局变量与使用它们的函数之间的关系。
-
把相关的变量作为类的属性封装到一起。
-
把相关的函数转换成新类的方法。