[譯] Python 學習 —— __init__() 方法 2

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

經過工廠函數對 __init__() 加以利用

咱們能夠經過工廠函數來構建一副完整的撲克牌。這會比枚舉全部52張撲克牌要好得多。在Python中,咱們有以下兩種常見的工廠方法:程序員

  • 定義一個函數,該函數會建立所需類的對象。編程

  • 定義一個類,該類有建立對象的方法。這是一個完整的工廠設計模式,正如設計模式書所描述的那樣。在諸如Java這樣的語言中,工廠類層次結構是必須的,由於該語言不支持獨立的函數。設計模式

在Python中,類不是必須的。只有當相關的工廠很是複雜的時候纔會顯現出優點。Python的優點就是當一個簡單的函數能夠作的更好時咱們決不強迫使用類層次結構。函數式編程

雖然這是一本關於面向對象編程的書,但函數真是一個好東西。這是常見也是最地道的Python。函數

若是須要的話,咱們老是能夠重寫一個函數爲適當的可調用對象,能夠將一個可調用對象重構到咱們的工廠類層次結構中。咱們將在第五章《使用Callables和Contexts》中學習可調用對象。學習

通常,類定義的優勢是經過繼承實現代碼重用。工廠類的函數就是包裝一些目標類層次結構和複雜對象的構造。若是咱們有一個工廠類,當擴展目標類層次結構的時候,咱們能夠添加子類到工廠類中。這給咱們提供了多態工廠類,不一樣的工廠類定義具備相同的方法簽名,能夠交替使用。ui

這個類級別的多態對於靜態編譯語言如Java或C++很是有用。編譯器能夠解決類和方法生成代碼的細節。設計

若是選擇的工廠定義不能重用任何代碼,則類層次結構在Python中不會有任何幫助。咱們能夠簡單的使用具備相同簽名的函數。code

如下是咱們各類Card子類的工廠函數:

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

這個函數經過rank數值和suit對象構建Card類。如今咱們能夠更簡單的構建牌了。咱們已經將構造過程封裝到一個單一的工廠函數中處理,容許應用程序在不知道精確的類層次結構和多態設計是如何工做的狀況下進行構建。

下面是如何經過這個工廠函數構建一副牌的示例:

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

它枚舉了全部的牌值和花色來建立完整的52張牌。

1. 錯誤的工廠設計和模糊的else子句

注意card()函數裏面的if語句結構。咱們沒有使用「一應俱全」的else子句來作任何處理;咱們只是拋出異常。使用「一應俱全」的else子句會引出相關的小爭論。

一方面,從屬於else子句的條件不能不言而喻,由於它可能隱藏着細微的設計錯誤。另外一方面,一些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)]

它起做用了嗎?若是if條件更復雜了呢?

一些程序員掃視的時候能夠理解這個if語句。其餘人將難以肯定是否全部狀況都正確執行了。

對於Python高級編程,咱們不該該把它留給讀者去演繹條件是否適用於else子句。對於菜鳥來講條件應該是顯而易見的,至少也應該是顯式的。

什麼時候使用「一應俱全」的else

儘可能的少使用,使用它只有當條件是顯而易見的時候。當有疑問時,顯式的使用並拋出異常。

避免含糊的else子句。

2. 簡單一致的使用elif序列

咱們的工廠函數card()是兩種常見工廠設計模式的混合物:

  • if-elif序列

  • 映射

爲了簡單起見,最好是專一於這些技術的一個而不是兩個。

咱們老是能夠用映射來代替elif條件。(是的,老是。但相反是不正確的;改變elif條件爲映射將是具備挑戰性的。)

如下是沒有映射的Card工廠:

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

咱們重寫了card()工廠函數。映射已經轉化爲額外的elif子句。這個函數有個優勢就是它比以前的版本更加一致。

3. 簡單的使用映射和類對象

在一些示例中,咱們可使用映射來代替一連串的elif條件。極可能發現條件太複雜,這個時候或許只有使用一連串的elif條件來表達纔是明智的選擇。對於簡單示例,不管如何,映射能夠作的更好且可讀性更強。

由於class是最好的對象,咱們能夠很容易的映射rank參數到已經構造好的類中。

如下是僅使用映射的Card工廠:

def card4(rank, suit):
    class_ = {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, NumberCard)
    return class_(rank, suit)

咱們已經映射rank對象到類中。而後,咱們給類傳遞rank值和suit值來建立最終的Card實例。

最好咱們使用defaultdict類。不管如何,對於微不足道的靜態映射不會比這更簡單了。看起來像下面代碼片斷那樣:

defaultdict(lambda: NumberCard, {1: AceCard, 11: FaceCard, 12: FaceCard, 12: FaceCard})

注意:defaultdict類默認必須是無參數的函數。咱們已經使用了lambda建立必要的函數來封裝常量。這個函數,不管如何,都有一些缺陷。對於咱們以前版本中缺乏1A13K的轉換。當咱們試圖增長這些特性時,必定會出現問題的。

咱們須要修改映射來提供能夠和字符串版本的rank對象同樣的Card子類。對於這兩部分的映射咱們還能夠作什麼?有四種常看法決方案:

  • 能夠作兩個並行的映射。咱們不建議這樣,可是會強調展現不可取的地方。

  • 能夠映射個二元組。這個一樣也會有一些缺點。

  • 能夠映射到partial()函數。partial()函數是functools模塊的一個特性。

  • 能夠考慮修改咱們的類定義,這種映射更容易。能夠在下一節將__init__()置入子類定義中看到。

咱們來看看每個具體的例子。

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)

這並不可取的。它涉及到重複映射鍵1111213序列。重複是糟糕的,由於在軟件更新後並行結構依然保持這種方式。

不要使用並行結構

並行結構必須使用元組或一些其餘合適的集合來替代。

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類層次結構來添加額外的Card子類時,咱們能夠看到它是如何被修改或被擴展。

rank值映射到一個類對象的確讓人感受奇怪,且只有類初始化所需兩個參數中的一個。將牌值映射到一個簡單的類或沒有提供一些混亂參數(但不是全部)的函數對象彷佛會更合理。

3.3. partial函數解決方案

相比映射到函數的二元組和參數之一,咱們能夠建立一個partial()函數。這是一個已經提供一些(但不是全部)參數的函數。咱們將從functools庫中使用partial()函數來建立一個帶有rank參數的partial類。

如下是將rank映射到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)

映射將rank對象與partial()函數聯繫在一塊兒,並分配給part_class。這個partial()函數能夠被應用到suit對象來建立最終的對象。partial()函數是一種常見的函數式編程技術。它在咱們有一個函數來替代對象方法這一特定的狀況下使用。

不過整體而言,partial()函數對於大多數面向對象編程並無什麼幫助。相比建立partial()函數,咱們能夠簡單地更新類的方法來接受不一樣組合的參數。partial()函數相似於給對象建立一個流暢的接口。

3.4. 連貫的工廠類接口

在某些狀況下,咱們設計的類在方法使用上定義好了順序,按順序求方法的值很像partial()函數。

在一個對象表示法中咱們可能會有x.a().b()。咱們能夠把它當成x(a, b)x.a()函數是等待b()的一類partial()函數。咱們能夠認爲它就像x(a)(b)那樣。

這裏的概念是,Python給咱們提供兩種選擇來管理狀態。咱們既能夠更新對象又能夠建立有狀態性的(在某種程度上)partial()函數。因爲這種等價,咱們能夠重寫partial()函數到一個流暢的工廠對象中。使得rank對象的設置爲一個流暢的方法來返回self。設置suit對象將真實的建立Card實例。

如下是一個流暢的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)

rank()方法更新構造函數的狀態,suit()方法真實的建立了最終的Card對象。

這個工廠類能夠像下面這樣使用:

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

首先,咱們建立一個工廠實例,而後咱們使用那個實例建立Card實例。這並無實質性改變__init__()Card類層次結構中的運做方式。然而,它確實改變了咱們應用程序建立對象的方式。

相關文章
相關標籤/搜索