注:原書做者 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設計模式的一個例子。
繼承:該設計模式是現有的集合類,是普通子類的定義。
多態:從頭開始設計。咱們將在第六章看看《建立容器和集合》。
這三個概念是面向對象設計的核心。在設計一個類的時候咱們必須老是這樣作選擇。
如下是封裝設計,其中包含一個內部集合:
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
對象。Deck
的pop()
方法簡單的委託給封裝好的list
對象。
而後咱們能夠經過下面這樣的代碼建立一個Hand
實例:
pythond = Deck() hand = [d.pop(), d.pop()]
通常來講,Facade設計模式或封裝好方法的類是簡單的被委託給底層實現類的。這個委託會變得冗長。對於一個複雜的集合,咱們能夠委託大量方法給封裝的對象。
封裝的另外一種方法是繼承內置類。這樣作的優點是沒有從新實現pop()
方法,由於咱們能夠簡單地繼承它。
pop()
的優勢就是不用寫過多的代碼就能建立類。在這個例子中,繼承list
類的缺點是提供了一些咱們不須要的函數。
下面是繼承內置list
的Deck
定義:
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
繼承的其餘方法也能一塊兒工做。
在賭場中,牌一般從牌盒發出,裏面有半打喜憂參半的撲克牌。這個緣由使得咱們有必要創建本身版本的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無縫地集成——基本的特殊方法》中咱們會回到這個問題。
理想狀況下,__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())
對於單元測試,在一個聲明中使用這種方式一般有助於構建複合對象。更重要的是,這種簡單、一步的計算來構建複合對象有利於下一部分的序列化技術。