《Python面向对象编程指南》——1.7 简单的组合对象

1.7 简单的组合对象

一个组合对象也可以称作容器。我们会从一个简单的组合对象开始介绍:一副牌。这是一个基本的集合对象。我们的确可以简单地使用一个list来代替一副牌(deck)对象。

在设计一个类之前,我们需要考虑这样的一个问题:简单地使用list是合适的做法吗?

可以使用random.shuffle()函数完成洗牌操作,使用deck.pop()来完成发牌操作。

一些程序员可能会过早定义新类,正如像使用内置类一样,违反了一些面向对象的设计原则。比如像下面的这个设计。

d= [card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart,
Spade)]
random.shuffle(d)
hand= [ d.pop(), d.pop() ]

可如果业务逻辑这么简单的话,为什么要定义新类?

这里没有明确的答案。类定义的一个优势是:类给对象提供了简单的、不需要实现的接口。正如之前在对工厂的设计讨论时所看到的,对于Python来说,类并不是必需的。

在之前的例子中,有两个关于deck的使用实例而且类定义似乎并不能过于简化。这有个很大的好处是它隐藏了具体的实现。而由于细节过于细微因此暴露它们并不需要太高的维护成本。本章主要专注于__init__()方法,因此接下来会讨论一些关于如何创建和初始化一个集合的设计。

设计集合类,通常有如下3种策略。

  • 封装:这个设计是基于现有集合类来定义一个新类,属于外观模式的一个使用场景。
  • 扩展:这个设计是对现有集合类进行扩展,通常使用定义子类的方式来实现。
  • 创建:即重新设计。在第6章“创建容器和集合”中我们会深入探讨。

这3个方面是面向对象设计的核心。我们在设计一个类时,总需要谨慎考虑再做出选择。

1.7.1 封装集合类

以下是对内部集合进行封装的设计。

class Deck:
  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()

我们已经定义了Deck类,内部实际调用的是list对象。Deck类的pop()方法只是对list对象相应函数的调用。

我们可以使用以下代码来创建一个Hand对象:

d= Deck()
hand= [ d.pop(), d.pop() ]

一般来说,外观模式或者封装类中的方法实现只是对底层对象相应函数的代理调用。有时候这样的代理未免显得有些多余,因为对于复杂的集合,我们需要代理大量的函数来更完整地封装这个底层对象。

1.7.2 扩展集合类

类设计的另一个选择是扩展现有类。这样做的好处是不需要再重新实现已有的pop()方法了,只需简单地继承即可。重用pop()方法的好处是,无需编写太多代码就可以创建一个类。在这个例子中,扩展list类引入了很多我们实际并不需要的函数。

以下代码演示了基于对内部集合类扩展的Deck类的定义。

class Deck2( list ):
  def __init__( self ):
    super().__init__( card6(r+1,s) for r in range(13) for s in
(Club, Diamond, Heart, Spade) )
    random.shuffle( self )

在一些情形下,在子类中需要显式调用基类的函数来完成适当的实现。关于这一点,在接下来的章节中会看到其他一些例子。

我们使用了基类中的__init__()函数来初始化list对象进而构造了一个对象集合。然后进行洗牌操作。pop()函数只需继承自list集合就可以很好地工作了,其他函数也一样。

1.7.3 可适应更多需求的另一种设计

在玩牌时,牌通常会从一个发牌机中取出,这个容器通常包含了混在一起的6副牌。这样就需要我们来创建一个自定义的Deck类而不再只是简单地从list对象继承。

进一步说,发牌机并未完全发牌,而是插入一个标记牌。由于有一张标记牌,有些牌就被有效地分开了。

以下是一个Deck类的定义,包含了多副牌,每副牌有52张牌。

class Deck3(list):
  def __init__(self, decks=1):
    super().__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.randint(1,52)
    for i in range(burn): self.pop()

这里我们使用了基类的__init__()函数来创建一个空集合。然后调用self.extend()函数来把多副牌加载到发牌机中。由于我们没有在子类重写super().extend()函数,因为我们也可以直接调用基类中相应的实现。

我们也可以使用更底层的表达式生成器通过调用super().__init__()函数来实现,如以下代码所示。

( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart,
Spade) for d in range(decks) )

这个类提供了一副牌Card实例的集合,可以用来模拟21点中的发牌机的发牌过程。

当销牌时,有一个特殊的过程。在我们设计玩家的纸牌计数策略时,也要考虑到这个细节。

时间: 2024-08-01 21:07:55

《Python面向对象编程指南》——1.7 简单的组合对象的相关文章

《Python面向对象编程指南》——导读

前 言 本书主要介绍Python语言的高级特性,特别是如何编写高质量的Python程序.这通常意味着编写高性能且拥有良好可维护性的程序.同时,我们也会探究不同的设计方案并确定究竟是哪种方案提供了最佳性能.而对于一些正在寻找解决方案的问题,这也是一种很好的方式. 本书的大部分内容将介绍一种给定设计的不同替代方案.一些方案性能更好,另一些方案更加简单或者更加适合于特定领域的问题.最重要的是,找到最好的算法和最优的数据结构,以最少的开销换取最大的价值.时间就是金钱,高效的程序会为它们的用户创造更多的价

《Python面向对象编程指南》——1.5 通过工厂函数调用__init()__

1.5 通过工厂函数调用__init()__ 我们可以使用工厂函数来完成所有Card对象的创建,这比枚举52张牌的方式好很多.在Python中,实现工厂有两种途径. 定义一个函数,返回不同类的对象. 定义一个类,包含了创建对象的方法.这是完整的工厂设计模式,正如设计模式书中提到的.在类似Java这样的语言里,工厂类层次结构是必需的,因为语言本身不支持可以脱离类而单独存在的函数. 在Python里,类定义不是必需的.仅当特别复杂的情形,工厂类才是不错的选择.Python的优势之一是,对于只需要简单

《Python面向对象编程指南》——第1部分 用特殊方法实现Python风格的类 第1章 __init__()方法 1.1 隐式的基类——object

第1部分 用特殊方法实现Python风格的类 init()方法 与Python无缝集成--基本特殊方法 属性访问.特性和修饰符 抽象基类设计的一致性 可调用对象和上下文的使用 创建容器和集合 创建数值类型 装饰器和Mixins--横切方面 用特殊方法实现 Python风格的类 通过重写特殊方法来完成对Python内部机制的调用,在Python中是很普遍的.例如len()函数就可以重写一个类的__len__()方法. 这意味着对于像(len(x))这样的通用公共接口,任何类(例如,声明一个类叫ti

《Python面向对象编程指南》——1.12 更多的__init__()技术

1.12 更多的__init__()技术 我们再来看一下其他一些更高级的__init__()技术的应用.相比前面的介绍,它们的应用场景不是特别常见. 以下是Player类的定义,初始化使用了两个策略对象和一个table对象.这个__init__()函数看起来不够漂亮. class Player: def __init__( self, table, bet_strategy, game_strategy ): self.bet_strategy = bet_strategy self.game_

《Python面向对象编程指南》——2.8 __new__()方法和不可变对象

2.8 __new__()方法和不可变对象 __new__方法的一个用途是初始化不可变对象.__new__()方法中允许创建未初始化的对象.这允许我们在__init__()方法被调用之前先设置对象的属性. 由于不可变类的__init__()方法很难重载,因此__new__方法提供了一种扩展这种类的方法. 下面是一个错误定义的类,我们定义了float的一个包含单位信息的版本. class Float_Fail( float ): def __init__( self, value, unit ):

《Python面向对象编程指南》——2.10 总结

2.10 总结 我们已经介绍了许多基本的特殊方法,它们是我们在设计任何类时的基本特性.这些方法已经包含在每个类中,只是它们的默认行为不一定能满足我们的需求. 我们几乎总是需要重载__repr__().__str__().和__format__().这些方法的默认实现不是非常有用. 我们几乎不需要重载__bool__()方法,除非我们想自定义集合.这是第6章"创建容器和集合"的主题. 我们常常需要重载比较运算符和__hash__()方法.默认的实现只适合于比较简单不可变对象,但是不适用于

《Python面向对象编程指南》——2.7 __del__()方法

2.7 __del__()方法 __del__()方法有一个让人费解的使用场景. 这个方法的目的是在将一个对象从内存中清除之前,可以有机会做一些清理工作.如果使用上下文管理对象或者with语句来处理这种需求会更加清晰,这也是第5章"可调用对象和上下文的使用"的内容.对于Python的垃圾回收机制而言,创建一个上下文比使用__del__()更加容易预判. 但是,如果一个Python对象包含了一些操作系统的资源,__del__()方法是把资源从程序中释放的最后机会.例如,引用了一个打开的文

《Python面向对象编程指南》——2.5 __bytes__()方法

2.5 __bytes__()方法 只有很少的情景需要我们把对象转换为字节.在第2部分"持久化和序列化"中,我们会详细探讨这个主题. 通常,应用程序会创建一个字符串,然后使用Python的IO类内置的编码方法将字符串转换为字节.对于大多数情况,这种方法就足够了.只有当我们自定义一种新的字符串时,我们会需要定义这个字符串的编码方法. 依据不同的参数,bytes()函数的行为也不同. bytes(integer):返回一个不可变的字节对象,这个对象包含了给定数量的0x00值. bytes(

《Python面向对象编程指南》——2.2 __format__()方法

2.2 __format__()方法 string.format()和内置的format()函数都使用了__format__()方法.它们都是为了获得给定对象的一个符合要求的字符串表示. 下面是给__format__()传参的两种方式. someobject.__format__(""):当应用程序中出现format(someobject)或者"{0}".format(someobject)时,会默认以这种方式调用__format__().在这些情况下,会传递一个空