[譯] 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())

對於單元測試,在一個聲明中使用這種方式一般有助於構建複合對象。更重要的是,這種簡單、一步的計算來構建複合對象有利於下一部分的序列化技術。

相關文章
相關標籤/搜索