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对象,可以使用备忘录模式来存放一些统计的数据。