第一部分 用特殊方法实现Python风格的类
为了实现更好的可扩展性,Python语言提供了大量的特殊方法,它们大致分为以下几类。
- 特性访问
- 可调用对象
- 集合
- 数字
- 上下文
- 迭代器
第一章 使用__init()__方法
Python中一切事物皆对象!!!!!!
__init__()方法记住两点:
- __init()__(初始化)是对象生命周期的开始,每个对象必须正确初始化才能够正常的工作。
- __init__()可以赋值
对象的生命周期主要是有创建、初始化、销毁。
‘显示而非隐式’:对于每个__init__()方法,都应当显示的制定要初始化的变量。
每当创建一个对象,python会创建一个空对象,然后调用该对象的__init__()函数,提供了初始化的操作。
# 以21点为例作为说明。
class Card(object):
def __init__(self, suit, rank):
self.suit = suit
self.rank = rank
self.hard, self.soft = self._points()
class NumberCard(Card):
def _points(self):
return int(self.rank), int(self.rank)
class AceCard(Card):
def _points(self):
return 1, 11
class FaceCard(Card):
def _points(self):
return 10, 10
class Suit(object):
def __init__(self,name,symbol):
self.name = name
self.symbol = symbol
Club,Diamond,Heart,Spade = Suit('Club','♣'),Suit('Diamond','♦'),Suit('Heart','♥'),Suit('Spade','♠')
通过工厂函数来调用__init__():
def card(rank, suit):
if rank == 1:
return AceCard('A', suit)
elif 2 <= rank < 11:
return NumberCard(str(rank), suit)
elif 11 <= rank < 14:
name = {11: 'J', 12: 'Q', 13: 'K'}[rank]
return FaceCard(name, suit)
else:
raise Exception("rank out of range")
这个函数通过传入牌面值rank 和花色值suit来创建card对象.
deck = [card(rank, suit) for rank in range(1, 14) for suit in (Club, Diamond, Heart, Spade)]
print(deck[0].rank,deck[0].suit.symbol)
这段代码完成了52张牌对象的创建.
使用映射和类来简化设计.
由于类是第一级别的对象,从rank参数射到对象是很容易的事情.
下面的Card类工厂就是使用映射实现的版本.
def card4(rank,suit):
class_ = {1:AceCard,11:FaceCard,12:FaceCard,13:FaceCard}.get(rank,NumberCard)
return class_(rank,suit)
需要修改映射逻辑,除了提供Card子类,还需要提供rank对象的字符串结果.如何实现这两部分映射,有四种常见方案.
- 可以建立两个并行映射
- 可以映射为一个二元组.
- 可以映射为partial()函数.
- 可以考虑修改类定义的完成映射逻辑.
1.并行映射
def card5(rank,suit):
class_ = {1:AceCard,11:FaceCard,12:FaceCard,13:FaceCard}.get(rank,NumberCard)
rank_str = {1:'A',11: 'J', 12: 'Q', 13: 'K'}.get(rank,str(rank))
return class_(rank_str,suit)
这样是不值得做的,带来映射键1,11,12,13的逻辑重复.
不要使用并行结构,并行结构应该被元祖或者一些更好的组合所代替
- 映射到一个牌面值的元组
def card6(rank,suit):
class_,rank_str= {
1:(AceCard,'A'),
11:(FaceCard,'J'),
12:(FaceCard,'Q'),
13:(FaceCard,'K')
}.get(rank,(NumberCard,str(rank)))
return class_(rank_str,suit)
从rank值映射到类对象时很少见的,而且两个参数只有一个用于对象的初始化.从rank映射到一个相对简单的类或者是函数对象,而不必提供目的不明确的参数,这才是明智的选择.
3.partial 函数设计
def card7(rank,suit):
from functools import partial
part_class = {
1:partial(AceCard,'A'),
11:partial(FaceCard,'J'),
12:partial(FaceCard,'Q'),
13:partial(FaceCard,'K')
}.get(rank,partial(NumberCard,str(rank)))
return part_class(suit)
通过调用partial()函数然后复制给part_class,完成于rank对象的管的关联,可以使用同样的方式来创建suit对象,并且完成最终的Card对象的创建.partial()函数的使用在函数时编程中是很常见的.当时用的是函数而非对象方法的时候就可以考虑使用.
大致上,partial()函数在面向对象编程中不是很常用,我们可以简单的的提供构造函数不同版本来做相同的事情.partial()函数和构造对象时的流畅接口很类似.
- 工厂模式的流畅的API设计
有时候我们定义类中的方法必须按照特定的顺序来调用.这种顺序调用的方法和创建 partial() 函数的方式非常类似.
我们可以在流畅接口函数中设置可以返回self值的rank对象,然后传入花色类从而创建Card实例/
以下是Card工厂流畅接口的定义,包含两个函数,他们必须按照顺序调用.
class CardFactory(object):
def rank(self,rank):
self.class_,self.rank_str = {
1:(AceCard,'A'),
11:(FaceCard,'J'),
12:(FaceCard,'Q'),
13:(FaceCard,'K')
}.get(rank,(NumberCard,str(rank)))
def suit(self,suit):
return self.class_(self.rank_str,suit)
先使用rank()函数更新了构造函数的状态,然后通过suit()函数创造了 最终的Card对象.
def A (rank):
a,b ={ # 本身为一个字典的传递值.返回对应的值.是dict的get方法
1: (AceCard, 'A'),
11: (FaceCard, 'J'),
12: (FaceCard, 'Q'),
13: (FaceCard, 'K')
}.get(rank, (NumberCard, str(rank)))
return a,b # 返回的是一个tuple(),a 为 , b 为'3'
a = A(3)
print(a)
我们先实例化一个工厂对象,然然后在创建Card实例,这用方式没有利用__init__() 在Card类层级结构的作用,改变的是调用者创建创建对象的方式.
在每个子类中实现__init__()方法
以下代码演示了如何把__init__()方法提到基类Card中实现的过程.然后在子类中可以重用基类的实现.
class Card(object):
def __init__(self, rank, suit, hard, soft):
self.rank = rank
self.suit = suit
self.hard = hard
self.soft = soft
class NumberCard(Card):
def __init__(self, rank, suit):
super().__init__(str(rank), suit, rank, rank)
class AceCard(Card):
def __init__(self, rank, suit):
super(AceCard, self).__init__("A", suit, 1, 11)
class FaceCard(Card):
def __init__(self, rank, suit):
super(FaceCard, self).__init__({11: 'J',
12: 'Q',
13: 'K'}[rank], suit, 10, 10)
def card10(rank,suit):
if rank == 1:
return AceCard(rank,suit)
elif 2<= rank < 11:
return NumberCard(rank,suit)
elif 11<= rank <14:
return FaceCard(rank,suit)
else:
raise Exception('Rank out of range')
在这里重构了基类中的__init__
,虽然将它复杂化,但是这样的权衡是正常的.
使用工厂函数封装的复杂性
在 `__init__()`方法和工厂函数之间存在一些权衡,通常直接调动比'程序员友好'的`__init__()`函数并把复杂性分发给工厂函数更好.当需要封装复杂的构造函数逻辑时,考虑使用工厂函数则更好.
简单的组合对象
一个组合对象也可以称作容器.
如果业务逻辑相对简单,为什么定义新类?
类的定义的一个优势是:
- 类给对象提供了简单的,不需要实现的接口.
设计集合类,通常是下面三种策略:
- 封装:这个实际是基于现有集合类来定义一个新类,属于外观模式的一个使用场景.
- 扩展:这个设计是对现有集合类进行扩展,通常使用定义子类的方式来实现.
- 创建:即重新设计.
以上是面向对象设计的核心.
封装集合类
以下是对内部集合进行封装设计.
import random
class Deck(object):
def __init__(self):
self._cards = [card6(r+1,s) for r in range(13) for s in (Club,Diamond,Heart,Spade)]
random.shuffle(self._cards)
def pop(self):
return self._cards.pop()
d = Deck()
hand = [d.pop(),d.pop()]
一般来说买外观模式或者封装类中的方法实现只是对底层对象相应函数的代理调用.
class Desk3(list):
def __init__(self, decks=1):
super(Desk3, self).__init__()
for i in range(decks):
self.extend(card6(r + 1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade))
random.shuffle(self)
burn = random.random(1,52)
for i in range(burn):
self.pop()
这里我们使用了基类的 __init__()
函数来创建了一个空集合,然后调用了 self.extrend()
来吧多副牌加载到发牌机中.
复杂的组合对象
模拟打牌策略
class Hand:
def __init__(self,dealer_card):
self.dealer_card = dealer_card
self.cards = []
def hard_total(self):
return sum(c.hard for c in self.cards)
def soft_total(self):
return sum(c.soft for c in self.cards)
d = Deck()
h = Hand(d.pop())
h.cards.append(d.pop())
h.cards.append(d.pop())
需要一个一个的添加非常不方便
完成组合对象的初始化
__init__()
初始化方法应当返回一个完成的对象,当然这个是理想的情况.而这样也带来复杂性,因为要创建的对象内部可能包含了集合,集合里面又包含了其他对象.
通常考虑使用一个流畅的接口来完成逐个讲对象添加到集合的操作,同时将集合对象作为构造函数来完成初始化.例如:
class Hand2:
def __init__(self, dealer_card, *cards):
self.dealer_card = dealer_card
self.cards = list(cards)
def hard_total(self):
return sum(c.hard for c in self.cards)
def soft_total(self):
return sum(c.soft for c in self.cards)
d = Deck()
h = Hand2(d.pop(),d.pop(),d.pop(),d.pop())
print(h.cards)
不带__init__
方法的无状态对象
一个策略对象以插件的形式复合在主对象上来完成一种算法或逻辑.它或许以来主对象中的数据,策略对象自身并不携带任何数据.通常策略类会和亨元设计模式一起使用:在策略对象中避免内部存储.所需要的值都从策略对象方法参数传入.策略对象自身是无状态的.可以把它看做是一系列函数的集合.
这里定义了一个类给Player实例提供了游戏的选择模式,以下这个策略包括拿牌和下注.
class GameStrategy:
def insurnace(self, hand):
return False
def split(self, hand):
return False
def double(self, hand):
return False
def hit(self, hand):
return False
每个函数需要传入已有的Hand对象,函数逻辑所需要的数据基于现有的可用信息.意味着数据来自于庄家跟玩家的手牌.
一起其他的类定义
玩家有两张策略:打牌和下注.每个Player实例回合模拟器进行很多次交互.我们这里把这个模拟器命名为Table
Table类的职责需要配合Player实例完成以下事件:
- 玩家必须要基于玩牌策略初始化一个牌局.
- 随后玩家会得到一手牌
- 如果
以下是Table类中投注和牌的逻辑处理相关的代码
class Table:
def __init__(self):
# 生成52张牌
self.deck = Deck()
def place_bet(self, amount):
print('Bet', amount)
def get_hand(self):
try:
# self.hand = Hand2(d.pop(), d.pop(), d.pop())
# self.hole_card = d.pop() 书上是这么写的我认为不对,改为下面写法
self.hand = Hand2(self.deck.pop(), self.deck.pop(), self.deck.pop())
self.hole_card = self.deck.pop()
except IndexError:
# Out of cards: need to shuffle
self.deck = Deck()
return self.get_hand()
print('Deal', self.hand)
return self.hand
# 没有看明白hand从何而来,所以也未找到insure的方法。估计是写错了。
def can_insure(self, hand):
return hand.dealer_card.insure
class BettingStrategy:
def bet(self):
raise NotImplementedError('No bet method')
def record_win(self):
pass
def record_lose(self):
pass
class Flat(BettingStrategy):
def bet(self):
return 1
上面的那一段代码还未看懂需要以后再来看一遍.
多策略的__init__()
方法
class Hand4:
def __init__(self, *args, **kwargs):
print(len(args),args,kwargs)
if len(args) == 1 and isinstance(args[0], Hand4):
other = args[0]
self.dealer_card = other.dealer_card
self.cards = other.cards
elif len(args) == 2 and isinstance(args[0], Hand4) and 'split' in kwargs:
# Split an existing hand
other, card = args
self.dealer_card = other.dealer_card
self.cards = [other.cards[kwargs['split']], card]
elif len(args) == 3:
# Bulid a fresh ,new hand
dealer_card,*cards = args
self.dealer_card = dealer_card
self.cards = list(cards)
else:
raise TypeError('Invaild constructor args= {0!r} kw={1!r}'.format(args,kwargs))
def __str__(self):
return ','.join(map(str,self.cards))
d = Deck()
h = Hand4(d.pop(),d.pop(),d.pop())
print(h)
# s1 = Hand4(h,d.pop(),split = 0)
# s2 = Hand4(h,d.pop(),split = 1)
class Hand5:
def __init__(self,dealer_card,*cards):
self.dealer_card = dealer_card
self.cards = list(cards)
@staticmethod
def freeze(other):
hand = Hand5(other.dealer_card,*other.cards)
return hand
@staticmethod
def split(other,card0,card1):
hand0 = Hand5(other.dealer_card,other.cards[0],card0)
hand1 = Hand5(other.dealer_card,other.cards[1],card1)
return hand0,hand1
def __str__(self):
return ','.join(map(str,self.cards))
d = Deck()
h = Hand5(d.pop(),d.pop(),d.pop())
s1,s2 = Hand5.split(h,d.pop(),d.pop())
上面这段代码实现了:当第一轮发完牌是,dealer手牌有一张,Player手牌有两张,当手牌的两张牌相同的时候玩家可以选择分牌,将手中的的两张牌分为两组牌,继续进行游戏.然后发牌器会给Palyer每组牌中个发一张牌
更多的__init__()
技术
以下是Player类的定义,初始化使用两个策略对象和一个table对象
略