3.2 创建特性
特性是一个函数,看起来(在语法上)就是一个简单的属性。我们可以获取、设置和删除特性值,正如我们可以获取、设置和删除属性值。这里有一个重要的区别:特性是一个函数,而且可以被调用,而不仅仅是用于存储的对象的引用。
除了复杂程度,特性和属性的另一个区别在于,我们不能轻易地为已有对象添加新特性。但是默认情况下,我们可以很容易地给对象添加新属性。在这一点上,特性和属性有很大区别。
可以用两种方式来创建特性。我们可以使用@property修饰符或者使用property()函数。它们只是语法不同。我们会详细介绍使用修饰符的方式。
我们先看一下关于特性的两个基本设计模式。
- 主动计算(Eager Calculation):每当更新特性值时,其他相关特性值都会立即被重新计算。
- 延迟计算(Lazy calculation):仅当访问特性时,才会触发计算过程。
为了对比这两种模式,我们会把Hand对象的一些公共逻辑提到抽象基类中,如以下代码所示。
class Hand:
def str( self ):
return ", ".join( map(str, self.card) )
def repr( self ):
return "{class.name}({dealercard!r}, {cardsstr})".
format(
class=self._class,
_cards_str=", ".join( map(repr, self.card) ),
**self.__dict )
以上代码的逻辑只是定义了一些字符串的表示方法。在下面代码中定义了Hand类的子类,其中total属性的实现方式使用了延迟计算模式。
class HandLazy(Hand):
def _init( self, dealer_card, *cards ):
self.dealer_card= dealer_card
self._cards= list(cards)
@property
def total( self ):
delta_soft = max(c.soft-c.hard for c in self._cards)
hard_total = sum(c.hard for c in self._cards)
if hard_total+delta_soft <= 21: return hard_total+delta_soft
return hard_total
@property
def card( self ):
return self._cards
@card.setter
def card( self, aCard ):
self._cards.append( aCard )
@card.deleter
def card( self ):
self._cards.pop(-1)
Hand_Lazy类使用了一个Cards对象的集合来初始化Hand对象。其中total特性被定义为一个方法,仅当被调用时才会计算总值。另外,也定义了一些其他特性来更新手中的纸牌。card特性可以用来获取、设置或删除手中的牌,我们会在特性的setter和deleter部分介绍它们。
我们可以创建一个Hand对象,total看起来就是一个简单的属性。
>>> d= Deck()
>>> h= Hand_Lazy( d.pop(), d.pop(), d.pop() )
>>> h.total
19
>>> h.card= d.pop()
>>> h.total
29
当每次获取总值时,都会重新扫描每张牌并完成延迟计算,这个过程也是非常耗时的。
3.2.1 主动计算特性
以下是Hand类的子类,其中的total属性的实现方式为主动计算,每当有新牌添加时,total属性值都会被重新计算。
class HandEager(Hand):
def _init( self, dealer_card, *cards ):
self.dealer_card= dealer_card
self.total= 0
self._delta_soft= 0
self._hard_total= 0
self._cards= list()
for c in cards:
self.card = c
@property
def card( self ):
return self._cards
@card.setter
def card( self, aCard ):
self._cards.append(aCard)
self._delta_soft = max(aCard.soft-aCard.hard,
self._delta_soft)
self._hard_total += aCard.hard
self._set_total()
@card.deleter
def card( self ):
removed= self._cards.pop(-1)
self._hard_total -= removed.hard
# Issue: was this the only ace?
self._delta_soft = max( c.soft-c.hard for c in self._cards
)
self._set_total()
def _set_total( self ):
if self._hard_total+self._delta_soft <= 21:
self.total= self._hard_total+self._delta_soft
else:
self.total= self._hard_total
每当有新牌添加时,total属性值都会被更新。
在card特性的deleter中也需要相应维护total值的更新,即每当牌被移除时也会触发total属性值的计算过程。关于deleter的内容将会在下一部分中具体介绍。
有关对Hand类的两个子类(Hand_Lazy()和Hand_Eager())的调用代码逻辑是类似的。
d= Deck()
h1= Hand_Lazy( d.pop(), d.pop(), d.pop() )
print( h1.total )
h2= Hand_Eager( d.pop(), d.pop(), d.pop() )
print( h2.total )
两种情况下,客户端都只需使用total属性(不需要关心内部实现)。
使用特性的好处是,每当特性内部实现改变时调用方无需更改。使用getter和setter方法也可达到类似的目的。然而,getter和setter方法需要使用额外的语法来实现。以下是两个例子,其中一个使用了setter方法,而另一个则是使用了赋值运算符。
obj.set_something(value)
obj.something = value
由于使用赋值运算符(=)实现方式的代码意图会更显然一些,因此许多程序员会更倾向于使用这种方式。
3.2.2 setter和deleter特性
在之前的例子中,我们使用card特性来处理从牌对象到Hand对象的构造过程。
由于setter(和deleter)特性是基于getter属性创建的,因此先要使用如下代码定义一个getter特性。
@property
def card( self ):
return self._cards
@card.setter
def card( self, aCard ):
self._cards.append( aCard )
@card.deleter
def card( self ):
self._cards.pop(-1)
可以简单地使用如下代码完成发牌。
h.card= d.pop()
以上代码有一个缺陷,看起来像是使用一张牌来替换所有的牌。另外,它更新了可变对象的状态。可以使用iadd()特殊方法来使实现更简洁。我们会在第7章“创建数值类型”中详细介绍这类特殊方法。
对于当前示例,虽没有明确的理由需要使用deleter特性,仍可以使用它来做一些其他事情。我们可以使用它来移除最后一张被处理的牌,这可以作为分牌过程的一部分。
可以考虑使用如下代码作为split()的一个实现版本。
def split( self, deck ):
"""Updates this hand and also returns the new hand."""
assert self.cards[0].rank == self.cards[1].rank
c1= self._cards[-1]
del self.card
self.card= deck.pop()
h_new= self.__class( self.dealer_card, c1, deck.pop() )
return h_new
在以上代码所示的函数中,修改了传入的Hand对象并返回了新的Hand对象,以下是分牌的过程。
>>> d= Deck()
>>> c= d.pop()
>>> h= Hand_Lazy( d.pop(), c, c ) # Force splittable hand
>>> h2= h.split(d)
>>> print(h)
2♠, 10♠
>>> print(h2)
2♠, A♠
一旦有两张牌,就可以使用split()函数实现分牌并返回一个新的Hand对象。相应的,一张牌会从初始的Hand对象中移除。
这个版本的split()函数是有效的。然而,直接使用split()函数返回两个新的Hand对象会更好一些。而对于分牌前的Hand对象,可以使用备忘录模式来存放一些统计的数据。
