资讯专栏INFORMATION COLUMN

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

leanote / 1000人阅读

摘要:简单复合对象复合对象也可被称为容器。它难以序列化对象并像这样初始化来重建。接口仍然会导致多种方法计算。还要注意一些不完全遵循点规则的方法功能。逐步增加项目的方法和一步加载所有项目的方法是一样的。另一个方法就是之前那样的类定义。

  

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

在各个子类中实现__init__()

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

这需要共用的超类初始化以及特定的子类初始化。我们要谨遵Don"t Repeat Yourself(DRY)原则来保持代码可以被克隆到每一个子类中。

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

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__()做显式的引用。

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

工厂函数封装复杂性

在复杂的__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())

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/45381.html

相关文章

  • [] Python 学习 —— __init__() 方法 1

    摘要:第一是在对象生命周期中初始化是最重要的一步每个对象必须正确初始化后才能正常工作。第二是参数值可以有多种形式。基类对象的方法对象生命周期的基础是它的创建初始化和销毁。在某些情况下,这种默认行为是可以接受的。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python __init__()方法意义重大的原因有两个。第一是在对象生命...

    MobService 评论0 收藏0
  • [] Python 学习 —— __init__() 方法 2

    摘要:工厂类的函数就是包装一些目标类层次结构和复杂对象的构造。连贯的工厂类接口在某些情况下,我们设计的类在方法使用上定义好了顺序,按顺序求方法的值很像函数。这个工厂类可以像下面这样使用首先,我们创建一个工厂实例,然后我们使用那个实例创建实例。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python 通过工厂函数对 __init_...

    xiaotianyi 评论0 收藏0
  • [] Python 学习 —— __init__() 方法 4

    摘要:同时,有多个类级别的静态构造函数的方法。这个累赘,无论如何,是被传递到每个单独的对象构造函数表达式中。我们可能只有几个特定的担忧,提供额外关键字参数给构造函数。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python 没有__init__()的无状态对象 下面这个示例,是一个简化去掉了__init__()的类。这是一个常见...

    yvonne 评论0 收藏0
  • []PEP 380--子生成器的语法

    摘要:提议以下的新的生成器语法将被允许在生成器的内部使用其中表达式作用于可迭代对象,从迭代器中提取元素。子迭代器而非生成器的语义被选择成为生成器案例的合理泛化。建议如果关闭一个子迭代器时,引发了带返回值的异常,则将该值从调用中返回给委托生成器。 导语: PEP(Python增强提案)几乎是 Python 社区中最重要的文档,它们提供了公告信息、指导流程、新功能的设计及使用说明等内容。对于学习...

    fevin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<