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

1.5 通过工厂函数调用__init()__

我们可以使用工厂函数来完成所有Card对象的创建,这比枚举52张牌的方式好很多。在Python中,实现工厂有两种途径。

  • 定义一个函数,返回不同类的对象。
  • 定义一个类,包含了创建对象的方法。这是完整的工厂设计模式,正如设计模式书中提到的。在类似Java这样的语言里,工厂类层次结构是必需的,因为语言本身不支持可以脱离类而单独存在的函数。

在Python里,类定义不是必需的。仅当特别复杂的情形,工厂类才是不错的选择。Python的优势之一是,对于只需要简单地定义一个函数就能做到的事情没必要去定义类层次结构。

如果需要,我们总可以将函数重写为合适的可调用对象。进行工厂模式设计时,也可以将可调用对象进一步重构为工厂类的层次结构。我们将在第5章“可调用对象和上下文的使用”中详细介绍可调用对象。

从大体上来看,类定义的优势是:可以通过继承来使得代码可以被更好地重用。工厂类封装了类本身的层次结构以及对象构建的复杂过程。对于已有的工厂类,可以通过添加子类的方式来完成扩展,这样就获得了工厂类的多态设计,不同的工厂类名有相同的方法签名并可以在调用时通过替换对象来改变具体实现。

这种类级别的多态机制对于类似Java和C++这样的编译型语言来说是非常有用的,可以在编译器在生成目标代码时决定类和方法的实现细节。

如果可替代的工厂类并没有重用任何代码,那么类层次结构在Python中并没有多大作用,完全可以使用函数来替代。

以下是用来生成Card子类对象的一个工厂函数的例子。

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对象。

deck = [card(rank, suit)
  for rank in range(1,14)
    for suit in (Club, Diamond, Heart, Spade)]

这段代码枚举了所有牌面值和花色的牌,完成了52张牌对象的创建。

1.5.1 错误的工厂设计和模糊的else语句

这里需要注意card()函数里的if语句。并没有使用一个catch-all else语句做一些其他步骤,而只是单纯地抛出了一个异常。像这样的catch-all else语句的使用方式是有争议的。

一方面,else语句不能不做任何事情,因为这将隐藏微小的设计错误。另一方面,一些else语句的意图已经很明显了。

因此,避免模糊的else语句是非常重要的。

关于这一点,可以参照以下工厂函数的定义。

def card2( rank, suit ):
  if rank == 1: return AceCard( 'A', suit )
  elif 2 <= rank < 11: return NumberCard( str(rank), suit )
  else:
    name = { 11: 'J', 12: 'Q', 13: 'K' }[rank]
    return FaceCard( name, suit )

创建纸牌对象可以通过如下代码实现。

deck2 = [card2(rank, suit) for rank in range(13) for suit in (Club,
Diamond, Heart, Spade)]

这是最好的方式吗?如果if条件更复杂些呢?

一些程序员可以很快理解这样的if语句,而另一些则会纠结于是否要对if语句的逻辑做进一步划分。

作为高级的Python程序员,我们不应该把else语句的意图留给读者去推断,条件语句的意图应当是非常直接的。

1.5.2 使用elif简化设计来获得一致性

工厂方法card()中包括了两个很常见的结构。

  • if-elif序列。
  • 映射。

为了简单化,重构将是更好的选择。

我们总可以使用elif条件语句代替映射。(是的,总可以。反过来却不行;把elif条件转换为映射有时是有风险的。)

以下是没有使用映射Card工厂类的实现。

def card3( rank, suit ):
  if rank == 1: return AceCard( 'A', suit )
  elif 2 <= rank < 11: return NumberCard( str(rank), suit )
  elif rank == 11:
    return FaceCard( 'J', suit )
  elif rank == 12:
    return FaceCard( 'Q', suit )
  elif rank == 13:
    return FaceCard( 'K', suit )
  else:
    raise Exception( "Rank out of range" )

这里重写了card()工厂方法,将映射转换为了elif语句。比起前一个版本,这个函数在实现上获得了更好的一致性。

1.5.3 使用映射和类来简化设计

在一些情形下,可以使用映射而非这样的一个elif条件语句链。如果认为使用一个elif条件语句链是表达逻辑的唯一明智的方式,那么很容易会发现,它看起来很复杂。对于简单的情形,做同样的事情采用映射完成的代码可以更好地工作,而且代码的可读性也更强。

由于类是第1级别的对象,从rank参数映射到对象是很容易的事情。

这个Card工厂类就是使用映射实现的版本。

def card4( rank, suit ):
  class_= {1: AceCard, 11: FaceCard, 12: FaceCard,
    13: FaceCard}.get(rank, NumberCard)
  return class_( rank, suit )

我们把rank映射为对象,然后又把rank值和suit值作为参数传入Card构造函数来创建Card实例。

也可以使用一个defaultdict类,然而比起简单的静态映射其实并没有简化多少。下例就是它的实现。

defaultdict( lambda: NumberCard, {1: AceCard, 11: FaceCard, 12:
FaceCard, 13: FaceCard} )

defaultdict类的默认构造函数必须是无参的。我们使用了一个lambda构造函数作为常量的封装函数。这个函数有个很明显的缺陷,缺少从1到A和13到K的映射。当试图添加这段代码逻辑时,就遇到了个问题。

我们需要修改映射逻辑,除了提供Card子类,还需要提供rank对象的字符串结果。如何实现这两部分的映射?有4种常见的方案。

  • 可以建立两个并行的映射。此处并不推荐这种做法,后面的章节会说明为什么这样做是不值得的。
  • 可以映射为一个二元组。当然,这个方案也有一些弊端。
  • 可以映射为partial()函数。partial()函数是fun``ctools模块的一个功能。
  • 也可以考虑修改类定义来完成映射逻辑。在下一节里会介绍如何在子类中重写__init()__函数来完成这个方案。

对于每个方案我们会通过具体示例逐一演示。

1.并行映射

以下是此方案代码的基本实现。

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的逻辑重复。重复是糟糕的,因为软件更新后通常会带来对并行结构多余的维护成本。

2.映射到一个牌面值的元组

以下代码演示了如何映射到二元组的基本实现。

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 )

这个方案看起来还不错。并没有太多代码来完成特殊情形的处理。接下来我们会看到当需要修改Card类层次结构时:添加一个Card子类时,如何来修改和扩展。

从rank值映射为类对象是很少见的,而且两个参数中只有一个用于对象的初始化。从rank映射到一个相对简单的类或函数对象,而不必提供目的不明确的参数,这才是明智的选择。

3.partial函数设计

除了映射到二元组函数和只提供一个参数来实例化的方案外,我们还可以创建partial()函数。这个函数可以用来实现可选参数。我们会从functools库中使用partial()函数创建一个带有rank参数的部分类。

以下演示了如何建立从rank到partial()函数的映射来完成对象的初始化。

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()函数和构造对象时的流畅接口很类似。

4.工厂模式的流畅API设计

有时我们定义在类中的方法必须按特定的顺序来调用。这种按顺序调用的方法和创建partial()函数的方式非常类似。

假如有这样的函数调用x.a().b()。对于x(a,b)这个函数,放在partial()函数的实现就可以是先调用x.a()再调用b()函数,这种方式可以理解为x(a)(b)。

这意味着Python在管理状态方面提供了两种选择。我们可以直接更新对象或者对具有状态的对象使用partial()函数。由于两种方式是等价的,因而可以把partial()函数重构为工厂对象创建的流畅接口。我们在流畅接口函数中设置可以反馈self值的rank对象,然后传入花色类从而创建Card实例。

如下是Card工厂流畅接口的定义,包含了两个函数,它们必须按顺序调用。

class CardFactory:
  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)))
    return self
  def suit( self, suit ):
    return self.class_( self.rank_str, suit)

先是使用rank()函数更新了构造函数的状态,然后通过suit()函数创造了最终的Card对象。

这个工厂类可以以如下方式来使用。

card8 = CardFactory()
deck8 = [card8.rank(r+1).suit(s) for r in range(13) for s in (Club,
Diamond, Heart, Spade)]

我们先实例化一个工厂对象,然后再创建Card实例。这种方式并没有利用__init()__在Card类层次结构中的作用,改变的是调用者创建对象的方式。

时间: 2024-10-02 05:29:29

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

《Python面向对象编程指南》——1.4 使用__init()__方法创建常量清单

1.4 使用__init()__方法创建常量清单 我们可以为所有卡片的花色单独创建一个类.可在21点应用中,花色不是很重要,用一个字母来代替就可以. 这里使用花色的初始化作为创建常量对象的一个实例.很多情况下,应用会包括一个常量集合.静态常量也正构成了策略(Strategy)或状态(State)模式的一部分. 有些情况下,常量会在应用或配置文件的初始化阶段被创建.或者创建变量的行为是基于命令行参数的.我们会在第16章"使用命令行"中介绍应用初始化和启动的详细设计过程. Python中并

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

前 言 本书主要介绍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.3 在基类中实现init()方法

1.3 在基类中实现init()方法 通过实现__init()__方法来初始化一个对象.每当创建一个对象,Python会先创建一个空对象,然后调用该对象的__init()__函数.这个方法提供了对象内部变量以及其他一些一次性过程的初始化操作. 以下是关于一个Card类层次结构定义的一些例子.这里定义了一个基类和3个子类来描述Card类的基本信息.有两个变量是参数直接赋值的,另外两个参数是通过初始化方法计算来完成初始化的. class Card: def __init__( self, rank,

《Python面向对象编程指南》——1.6 在每个子类中实现__init()__方法

1.6 在每个子类中实现__init()__方法 正如介绍工厂函数那样,这里我们也先看一些Card类的设计实例.我们可以考虑重构rank数值转换的代码,并把这个功能加在Card类上.这样就可以把初始化的工作分发到每个子类来完成. 这通常需要在基类中完成一些公共的初始化逻辑,子类中完成各自特殊的初始化逻辑.我们需要遵守不要重复自己(Don't Repeat Yourself,DRY)的原则来防止子类中的代码重复. 以下代码演示了如何把初始化职责分发到各自的子类中. class Card: pass

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

1.7 简单的组合对象 一个组合对象也可以称作容器.我们会从一个简单的组合对象开始介绍:一副牌.这是一个基本的集合对象.我们的确可以简单地使用一个list来代替一副牌(deck)对象. 在设计一个类之前,我们需要考虑这样的一个问题:简单地使用list是合适的做法吗? 可以使用random.shuffle()函数完成洗牌操作,使用deck.pop()来完成发牌操作. 一些程序员可能会过早定义新类,正如像使用内置类一样,违反了一些面向对象的设计原则.比如像下面的这个设计. d= [card6(r+1

《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.6 比较运算符方法

2.6 比较运算符方法 Python有6个比较运算符.这些运算符分别对应一个特殊方法的实现.根据文档,运算符和特殊方法的对应关系如下所示. x < y调用x.__lt__(y). x <=y调用x.__le__(y). x == y调用x.__eq__(y). x != y调用x.__ne__(y). x > y调用x.__gt__(y). x >= y调用x.__ge__(y). 我们会在第7章"创建数值类型"中再探讨比较运算符. 对于实际上使用了哪个比较运算

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

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