[译] Python 学习 —— __init__() 方法 3

注:原书做者 Steven F. Lott,原书名为 Mastering Object-oriented Pythonpython

在各个子类中实现__init__()

当咱们看到建立Card对象的工厂函数,再看看Card类设计。我想咱们可能要重构牌值转换功能,由于这是Card类自身应该负责的内容。这会将初始化向下延伸到每一个子类。程序员

这须要共用的超类初始化以及特定的子类初始化。咱们要谨遵Don't Repeat Yourself(DRY)原则来保持代码能够被克隆到每个子类中。设计模式

下面的示例展现了每一个子类初始化的职责:app

pythonclass Card:
    pass

class NumberCard(Card):
    def  __init__(self, rank, suit):
        self.suit = suit
        self.rank = str(rank)
        self.hard = self.soft = rank

class AceCard(Card):
    def  __init__(self, rank, suit):
        self.suit = suit
        self.rank = "A"
        self.hard, self.soft =  1, 11

class FaceCard(Card):
    def  __init__(self, rank, suit):
        self.suit = suit
        self.rank = {11: 'J', 12: 'Q', 13: 'K'}[rank]
        self.hard = self.soft = 10

这还是清晰的多态。然而,缺少一个真正的共用初始化,会致使一些冗余。缺点在于重复初始化suit,因此必须将其抽象到超类中。各子类的__init__()会对超类的__init__()作显式的引用。dom

该版本的Card类有一个超类级别的初始化函数用于各子类,以下面代码片断所示:函数

pythonclass Card:
    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().__init__("A", suit, 1, 11)

class FaceCard(Card):
    def  __init__(self, rank, suit):
        super().__init__({11: 'J', 12: 'Q', 13: 'K' }[rank], suit, 10, 10)

咱们在子类和父类都提供了__init__()函数。好处是简化了咱们的工厂函数,以下面代码片断所示:单元测试

pythondef 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__()函数,而对工厂函数却有一些较小的改进。这是比较常见的权衡。测试

工厂函数封装复杂性ui

在复杂的__init__()方法和工厂函数之间有个权衡。最好就是坚持更直接,更少程序员友好的__init__()方法,并将复杂性推给工厂函数。若是你想封装复杂结构,工厂函数能够作的很好。设计

简单复合对象

复合对象也可被称为容器。咱们来看一个简单的复合对象:一副单独的牌。这是一个基本的集合。事实上它是如此基本,以致于咱们不用过多的花费心思,直接使用简单的list作为一副牌。

在设计一个新类以前,咱们须要问这个问题:使用一个简单的list是否合适?

咱们可使用random.shuffle()来洗牌和使用deck.pop()发牌到玩家手里。

一些程序员急于定义新类就像使用内置类同样草率,这很容易违反面向对象的设计原则。咱们要避免一个新类像以下代码片断所示:

pythond = [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中类并非一个硬性要求。

在前面的代码中,一副牌只有两个简单的用例和一个彷佛并不够简化的类定义。它的优点在于隐藏实现的细节,但细节是如此微不足道,揭露它们几乎没有任何意义。在本章中,咱们的关注主要放在__init__()方法上,咱们将看一些建立并初始化集合的设计。

设计一个对象集合,有如下三个整体设计策略:

  • 封装:该设计模式是现有的集合的定义。这多是Facade设计模式的一个例子。

  • 继承:该设计模式是现有的集合类,是普通子类的定义。

  • 多态:从头开始设计。咱们将在第六章看看《建立容器和集合》。

这三个概念是面向对象设计的核心。在设计一个类的时候咱们必须老是这样作选择。

1. 封装集合类

如下是封装设计,其中包含一个内部集合:

pythonclass 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对象。Deckpop()方法简单的委托给封装好的list对象。

而后咱们能够经过下面这样的代码建立一个Hand实例:

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

通常来讲,Facade设计模式或封装好方法的类是简单的被委托给底层实现类的。这个委托会变得冗长。对于一个复杂的集合,咱们能够委托大量方法给封装的对象。

2. 继承集合类

封装的另外一种方法是继承内置类。这样作的优点是没有从新实现pop()方法,由于咱们能够简单地继承它。

pop()的优势就是不用写过多的代码就能建立类。在这个例子中,继承list类的缺点是提供了一些咱们不须要的函数。

下面是继承内置listDeck定义:

pythonclass 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继承过来且工做完美。从list继承的其余方法也能一块儿工做。

3. 更多的需求和另外一种设计

在赌场中,牌一般从牌盒发出,里面有半打喜忧参半的扑克牌。这个缘由使得咱们有必要创建本身版本的Deck,而不是简单、纯粹的使用list对象。

此外,牌盒里的牌并不彻底发完。相反,会插入标记牌。由于有标记牌,有些牌会被保留,而不是用来玩。

下面是包含多组52张牌的Deck定义:

pythonclass 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()

在这里,咱们使用super().__init__()来构建一个空集合。而后,咱们使用self.extend()添加屡次52张牌。因为咱们在这个类中没有使用覆写,因此咱们可使用super().extend()

咱们还能够经过super().__init__(),使用更深层嵌套的生成器表达式执行整个任务。以下面代码片断所示:

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

这个类为咱们提供了一个Card实例的集合,咱们可使用它来模仿赌场21点发牌的盒子。

在赌场有一个奇怪的仪式,他们会翻开废弃的牌。若是咱们要设计一个记牌玩家策略,咱们可能须要效仿这种细微差异。

复杂复合对象

如下是21点Hand类描述的一个例子,很适合模拟玩家策略:

pythonclass 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)

在这个例子中,咱们有一个基于__init__()方法参数的self.dealer_card实例变量。self.cards实例变量是不基于任何参数的。这个初始化建立了一个空集合。

咱们可使用下面的代码去建立一个Hand实例

pythond = Deck()
h = Hand(d.pop())
h.cards.append(d.pop())
h.cards.append(d.pop())

缺点就是有一个冗长的语句序列被用来构建一个Hand的实例对象。它难以序列化Hand对象并像这样初始化来重建。尽管咱们在这个类中建立一个显式的append()方法,它仍将采起多个步骤来初始化集合。

咱们能够尝试建立一个接口,但这并非一件简单的事情,对于Hand对象它只是在语法上发生了变化。接口仍然会致使多种方法计算。当咱们看到第2部分中的《序列化和持久化》,咱们倾向于使用接口,一个类级别的函数,理想状况下,应该是类的构造函数。咱们将在第9章的《序列化和存储——JSON、YAML、Pickle、CSV和XML》深刻研究。

还要注意一些不彻底遵循21点规则的方法功能。在第二章《经过Python无缝地集成——基本的特殊方法》中咱们会回到这个问题。

1. 复杂复合对象初始化

理想状况下,__init__()方法会建立一个对象的完整实例。这是一个更复杂的容器,当你在建立一个包含内部其余对象集合的完整实例的时候。若是咱们能够一步就能构建这个复合对象,它将是很是有帮助的。

逐步增长项目的方法和一步加载全部项目的方法是同样的。

例如,咱们可能有以下面的代码片断所示的类:

pythonclass 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)

这个初始化一步就设置了全部实例变量。另外一个方法就是以前那样的类定义。咱们能够有两种方式构建一个Hand2对象。第一个示例一次加载一张牌到Hand2对象:

pythond = Deck()
P = Hand2(d.pop())
p.cards.append(d.pop())
p.cards.append(d.pop())

第二个示例使用*cards参数一步加载一序列的Card类:

pythond = Deck()
h = Hand2(d.pop(), d.pop(), d.pop())

对于单元测试,在一个声明中使用这种方式一般有助于构建复合对象。更重要的是,这种简单、一步的计算来构建复合对象有利于下一部分的序列化技术。

相关文章
相关标签/搜索