原博作者:langb2014

夜里未能读完,粘贴来,明天再读.

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

__init__()参数值可以有多种形式。

因为有很多种方式为 在我们接触object类的层次结构。

在这一章,我们看看不同形式的简单对象的初始化(例如:打牌)。在这之后,我们还可以看看更复杂的对象,就像包含集合的players

隐式超类——object

每一个Python类都隐含了一个超类:object。它是一个非常简单的类定义,几乎不做任何事情。我们可以创建 当我们自定义一个类,object

class X:
    pass

下面是和自定义类的一些交互:

>>> X.__class__
<class 'type'>
>>> X.__class__.__base__
<class 'object'>

我们可以看到该类是object

就像在每个方法中看到的那样,我们也看看从 基类对象的 对象生命周期的基础是它的创建、初始化和销毁。我们将创建和销毁推迟到后面章节的高级特殊方法中讲,目前只关注初始化。

所有类的超类pass 我们总是给对象添加属性,该对象为基类 class Rectangle: def area(self): return self.length * self.width

下面是与>>> r = Rectangle() >>> r.length, r.width = 13, 8 >>> r.area() 104

显然这是合法的,但这也是容易混淆的根源,所以也是我们需要避免的原因。

无论如何,这个设计给予了很大的灵活性,这样有时候我们不用在if语句的不恰当使用所造成的盘绕。虽然未初始化的属性可能是有用的,但也很有可能是糟糕设计的前兆。

《Python之禅》中的建议:

"显式比隐式更好。"

一个 非常差的多态

灵活和愚蠢就在一念之间。

当我们觉得需要像下面这样写的时候,我们正从灵活的边缘走向愚蠢:

if 'x' in self.__dict__:

或者:

try:
    self.x
except AttributeError:

是时候重新考虑API并添加一个通用的方法或属性。重构比添加 在超类中实现 我们通过实现__init__()方法。这个方法函数通常用来创建对象的实例变量并执行任何其他一次性处理。

下面是Card超类和三个子类,这三个子类是

 

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

class NumberCard(Card):
    def _points(self):
        return int(self.rank), int(self.rank)

class AceCard(Card):
    def _points(self):
        return 1, 11

class FaceCard(Card):
    def _points(self):
        return 10, 10

在这个示例中,我们提取Card超类中的通用初始化可以适用于三个子类AceCard 这是一种常见的多态设计。每一个子类都提供一个唯一的 如果我们为花色使用简单的字符,我们可以创建cards = [AceCard('A', '♠'), NumberCard('2','♠'), NumberCard('3','♠'),]

我们在列表中枚举出一些牌的类、牌值和花色。从长远来说,我们需要更智能的工厂函数来创建 使用 可以给牌定义花色类。在二十一点中,花色无关紧要,简单的字符串就可以。

我们使用花色构造函数作为创建常量对象示例。在许多情况下,我们应用中小部分对象可以通过常量集合来定义。小部分的静态对象可能是实现策略模式或状态模式的一部分。

在某些情况下,我们会有一个在初始化或配置文件中创建的常量对象池,或者我们可以基于命令行参数创建常量对象。我们会在第十六章《命令行处理》中获取初始化设计和启动设计的详细信息。

Python没有简单正式的机制来定义一个不可变对象,我们将在第三章《属性访问、特性和描述符》中看看保证不可变性的相关技术。在本示例中,花色不可变是有道理的。

下面这个类,我们将用于创建四个显而易见的常量:


class Suit:
    def __init__(self, name, symbol):
        self.name = name
        self.symbol = symbol

下面是通过这个类创建的常量:


Club, Diamond, Heart, Spade = Suit('Club','♣'), Suit('Diamond','♦'), Suit('Heart','♥'), Suit('Spade','♠')

现在我们可以通过下面展示的代码片段创建 cards = [AceCard('A', Spade), NumberCard('2', Spade), NumberCard('3', Spade),]

这个小示例的方法对于单个字符花色的代码来说并没有多大改进。在更复杂的情况下,会通过这个方式创建一些策略或状态对象。从小的静态常量池中复用对象使得策略或状态设计模式效率更高。

我们必须承认,在Python中这些对象并不是技术上一成不变的,它是可变的。进行额外的编码使得这些对象真正不可变可能会有一些好处。

无关紧要的不变性

不变性很有吸引力但却容易带来麻烦。有时候神话般的“恶意程序员”在他们的应用程序中通过修改常量值进行调整。从设计上考虑,这是非常愚蠢的。这些神话般的、恶意的程序员不会停止这样做。在Python中没有更好的方法保证没有白痴的代码。恶意程序员访问到源码并且修改它仅仅是希望尽可能轻松地编写代码来修改一个常数。

在定义不可变对象的类的时候最好不要挣扎太久。在第三章《属性访问、特性和描述符》中,我们将在有bug的程序中提供合适的诊断信息来展示如何实现不变性。

通过工厂函数对 __init__() 加以利用

我们可以通过工厂函数来构建一副完整的扑克牌。这会比枚举所有52张扑克牌要好得多。在Python中,我们有如下两种常见的工厂方法:

  • 定义一个函数,该函数会创建所需类的对象。

  • 定义一个类,该类有创建对象的方法。这是一个完整的工厂设计模式,正如设计模式书所描述的那样。在诸如Java这样的语言中,工厂类层次结构是必须的,因为该语言不支持独立的函数。

在Python中,类不是必须的。只有当相关的工厂非常复杂的时候才会显现出优势。Python的优势就是当一个简单的函数可以做的更好时我们决不强迫使用类层次结构。

虽然这是一本关于面向对象编程的书,但函数真是一个好东西。这是常见也是最地道的Python。

如果需要的话,我们总是可以重写一个函数为适当的可调用对象,可以将一个可调用对象重构到我们的工厂类层次结构中。我们将在第五章《使用Callables和Contexts》中学习可调用对象。

一般,类定义的优点是通过继承实现代码重用。工厂类的函数就是包装一些目标类层次结构和复杂对象的构造。如果我们有一个工厂类,当扩展目标类层次结构的时候,我们可以添加子类到工厂类中。这给我们提供了多态工厂类,不同的工厂类定义具有相同的方法签名,可以交替使用。

这个类级别的多态对于静态编译语言如Java或C++非常有用。编译器可以解决类和方法生成代码的细节。

如果选择的工厂定义不能重用任何代码,则类层次结构在Python中不会有任何帮助。我们可以简单的使用具有相同签名的函数。

以下是我们各种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")

这个函数通过suit对象构建 下面是如何通过这个工厂函数构建一副牌的示例:

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

它枚举了所有的牌值和花色来创建完整的52张牌。

1. 错误的工厂设计和模糊的else子句

注意if语句结构。我们没有使用“包罗万象”的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)]

它起作用了吗?如果 一些程序员扫视的时候可以理解这个 对于Python高级编程,我们不应该把它留给读者去演绎条件是否适用于 何时使用“包罗万象”的else

尽量的少使用,使用它只有当条件是显而易见的时候。当有疑问时,显式的使用并抛出异常。

避免含糊的 2. 简单一致的使用elif序列

我们的工厂函数

  • 映射

为了简单起见,最好是专注于这些技术的一个而不是两个。

我们总是可以用映射来代替elif条件为映射将是具有挑战性的。)

以下是没有映射的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")

我们重写了elif子句。这个函数有个优点就是它比之前的版本更加一致。

3. 简单的使用映射和类对象

在一些示例中,我们可以使用映射来代替一连串的elif条件来表达才是明智的选择。对于简单示例,无论如何,映射可以做的更好且可读性更强。

因为rank参数到已经构造好的类中。

以下是仅使用映射的 def card4(rank, suit): class_ = {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, NumberCard) return class_(rank, suit)

我们已经映射rank值和Card实例。

最好我们使用defaultdict(lambda: NumberCard, {1: AceCard, 11: FaceCard, 12: FaceCard, 12: FaceCard})

注意:lambda创建必要的函数来封装常量。这个函数,无论如何,都有一些缺陷。对于我们之前版本中缺少AK的转换。当我们试图增加这些特性时,一定会出现问题的。

我们需要修改映射来提供可以和字符串版本的Card子类。对于这两部分的映射我们还可以做什么?有四种常见解决方案:

  • 可以做两个并行的映射。我们不建议这样,但是会强调展示不可取的地方。

  • 可以映射个二元组。这个同样也会有一些缺点。

  • 可以映射到partial()函数是可以考虑修改我们的类定义,这种映射更容易。可以在下一节将 我们来看看每一个具体的例子。

    3.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)

    这并不可取的。它涉及到重复映射键1113序列。重复是糟糕的,因为在软件更新后并行结构依然保持这种方式。

    不要使用并行结构

    并行结构必须使用元组或一些其他合适的集合来替代。

    3.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子类时,我们可以看到它是如何被修改或被扩展。

    3.3. partial函数解决方案

    相比映射到函数的二元组和参数之一,我们可以创建一个functools库中使用rank参数的partial类。

    以下是将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()函数联系在一起,并分配给partial()函数可以被应用到partial()函数是一种常见的函数式编程技术。它在我们有一个函数来替代对象方法这一特定的情况下使用。

    不过总体而言,partial()函数,我们可以简单地更新类的方法来接受不同组合的参数。 3.4. 连贯的工厂类接口

    在某些情况下,我们设计的类在方法使用上定义好了顺序,按顺序求方法的值很像 在一个对象表示法中我们可能会有x(a, b)b()的一类x(a)(b)那样。

    这里的概念是,Python给我们提供两种选择来管理状态。我们既可以更新对象又可以创建有状态性的(在某种程度上)partial()函数到一个流畅的工厂对象中。使得self。设置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)

    suit()方法真实的创建了最终的 这个工厂类可以像下面这样使用:

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

    首先,我们创建一个工厂实例,然后我们使用那个实例创建__init__() 在各个子类中实现 当我们看到创建Card类设计。我想我们可能要重构牌值转换功能,因为这是 这需要共用的超类初始化以及特定的子类初始化。我们要谨遵Don't Repeat Yourself(DRY)原则来保持代码可以被克隆到每一个子类中。

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

    class 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
    

    这仍是清晰的多态。然而,缺乏一个真正的共用初始化,会导致一些冗余。缺点在于重复初始化__init__()会对超类的 该版本的class 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)

    我们在子类和父类都提供了def 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__()方法,并将复杂性推给工厂函数。如果你想封装复杂结构,工厂函数可以做的很好。

    简单复合对象

    复合对象也可被称为容器。我们来看一个简单的复合对象:一副单独的牌。这是一个基本的集合。事实上它是如此基本,以至于我们不用过多的花费心思,直接使用简单的 在设计一个新类之前,我们需要问这个问题:使用一个简单的 我们可以使用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中类并不是一个硬性要求。

    在前面的代码中,一副牌只有两个简单的用例和一个似乎并不够简化的类定义。它的优势在于隐藏实现的细节,但细节是如此微不足道,揭露它们几乎没有任何意义。在本章中,我们的关注主要放在 设计一个对象集合,有以下三个总体设计策略:

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

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

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

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

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

    我们已经定义了list对象。pop()方法简单的委托给封装好的 然后我们可以通过下面这样的代码创建一个d = Deck() hand = [d.pop(), d.pop()]

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

    2. 继承集合类

    封装的另一种方法是继承内置类。这样做的优势是没有重新实现 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)
    

    在某些情况下,为了拥有合适的类行为,我们的方法将必须显式地使用超类。在下面的章节中我们将会看到其他相关示例。

    我们利用超类的list对象来初始化单副扑克牌,然后我们洗牌。list继承过来且工作完美。从 3. 更多的需求和另一种设计

    在赌场中,牌通常从牌盒发出,里面有半打喜忧参半的扑克牌。这个原因使得我们有必要建立自己版本的list对象。

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

    下面是包含多组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()

    在这里,我们使用self.extend()添加多次52张牌。由于我们在这个类中没有使用覆写,所以我们可以使用 我们还可以通过(card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade) for d in range(decks))

    这个类为我们提供了一个 在赌场有一个奇怪的仪式,他们会翻开废弃的牌。如果我们要设计一个记牌玩家策略,我们可能需要效仿这种细微差别。

    复杂复合对象

    以下是21点class 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)

    在这个例子中,我们有一个基于self.dealer_card实例变量。 我们可以使用下面的代码去创建一个d = Deck() h = Hand(d.pop()) h.cards.append(d.pop()) h.cards.append(d.pop())

    缺点就是有一个冗长的语句序列被用来构建一个Hand对象并像这样初始化来重建。尽管我们在这个类中创建一个显式的 我们可以尝试创建一个接口,但这并不是一件简单的事情,对于 还要注意一些不完全遵循21点规则的方法功能。在第二章《通过Python无缝地集成——基本的特殊方法》中我们会回到这个问题。

    1. 复杂复合对象初始化

    理想情况下, 逐步增加项目的方法和一步加载所有项目的方法是一样的。

    例如,我们可能有如下面的代码片段所示的类:

    class 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对象:

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