Python中的__init__()方法整理中(兩種解釋)

farsun發表於2021-09-09


解釋一:看懂了就不用看第二種了

__init__()方法是Python學習當中重要的基礎知識,__init__()方法意義重大的原因有兩個。第一個原因是在物件生命週期中初始化是最重要的一步;每個物件必須正確初始化後才能正常工作。第二個原因是__init__()引數值可以有多種形式。


因為有很多種方式為__init__()提供引數值,對於物件建立有大量的用例,我們可以看看其中的幾個。我們想盡可能的弄清楚,因此我們需要定義一個初始化來正確的描述問題區域。在我們接觸__init__()方法之前,無論如何,我們都需要粗略、簡單地看看在Python中隱含的object類的層次結構。

隱含的超類——object

每一個Python類都隱含了一個超類:object。它是一個非常簡單的類定義,幾乎不做任何事情。我們可以建立object的例項,但是我們不能用它做太多,因為許多特殊的方法容易丟擲異常。

當我們自定義一個類,object則為超類。下面是一個類定義示例,它使用新的名稱簡單的繼承了object:


class X:  pass

下面是和自定義類的一些互動:


>>> X.__class__class 'type'>>>> X.__class__.__base__class 'object'>

我們可以看到該類是type類的一個物件,且它的基類為object。

就像在每個方法中看到的那樣,我們也看看從object繼承的預設行為。在某些情況下,超類特殊方法的行為是我們所想要的。在其他情況下,我們需要覆蓋這個特殊方法。
基類物件的init()方法


所有類的超類object,有一個預設包含pass的__init__()實現,我們不需要去實現__init__()。如果不實現它,則在物件建立後就不會建立例項變數。在某些情況下,這種預設行為是可以接受的。

我們總是給物件新增屬性,該物件為基類object的子類。思考以下類,需要兩個例項變數但不初始化它們:


class Rectangle:  def area(self):    return self.length * self.width

Rectangle類有一個使用兩個屬性來返回一個值的方法。這些屬性沒有初始化。這是合法的Python程式碼。它可以有效的避免專門設定屬性,雖然感覺有點奇怪,但是有效。

下面是於Rectangle類的互動:


>>> r = Rectangle()>>> r.length, r.width = 13, 8>>> r.area()104

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

無論如何,這個設計給予了很大的靈活性,這樣有時候我們不用在__init__()方法中設定所有屬性。至此我們走的很順利。一個可選屬性其實就是一個子類,只是沒有真正的正式宣告為子類。我們建立多型在某種程度上可能會引起混亂以及if語句的不恰當使用所造成的盤繞。雖然未初始化的屬性可能是有用的,但很有可能是糟糕設計的前兆。

《Python之禪》中的建議:

    "顯式比隱式更好。"

一個__init__()方法應該讓例項變數顯式。

可憐的多型

靈活和愚蠢就在一念之間。

當我們覺得需要像下面這樣寫的時候,我們正從靈活的邊緣走向愚蠢:

1 if 'x' in self.__dict__:

或者:


try:  self.xexcept AttributeError:

是時候重新考慮API並新增一個通用的方法或屬性。重構比新增if語句更明智。
在超類中實現init()

我們透過實現__init__()方法來初始化物件。當一個物件被建立,Python首先建立一個空物件,然後為那個新物件呼叫__init__()方法。這個方法函式通常用來建立物件的例項變數並執行任何其他一次性處理。

下面是Card類示例定義的層次結構。我們將定義Card超類和三個子類,這三個子類是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

在這個示例中,我們提取__init__()方法到超類,這樣在Card超類中的通用初始化可以適用於三個子類NumberCard、AceCard和FaceCard。

這是一種常見的多型設計。每一個子類都提供一個唯一的_points()方法實現。所有子類都有相同的簽名:有相同的方法和屬性。這三個子類的物件在一個應用程式中可以交替使用。

如果我們為花色使用簡單的字元,我們可以建立Card例項,如下所示:

1 cards = [AceCard('A', '?'), NumberCard('2','?'), NumberCard('3','?'),]

我們在列表中列舉出一些牌的類、牌值和花色。從長遠來說,我們需要更智慧的工廠函式來建立Card例項;用這個方法列舉52張牌無聊且容易出錯。在我們接觸工廠函式之前,我們看一些其他問題。
使用init()建立顯式常量

可以給牌定義花色類。在二十一點中,花色無關緊要,簡單的字串就可以。

我們使用花色建構函式作為建立常量物件的示例。在許多情況下,我們應用中小部分物件可以透過常量集合來定義。小部分的靜態物件可能是實現策略模式或狀態模式的一部分。

在某些情況下,我們會有一個在初始化或配置檔案中建立的常量物件池,或者我們可以基於命令列引數建立常量物件。我們會在第十六章《透過命令進行復制》中獲取初始化設計和啟動設計的詳細資訊。

Python沒有簡單正式的機制來定義一個不可變物件,我們將在第三章《屬性訪問、方法屬性和描述符》看看保證不可變性的相關技術。在本示例中,花色不可變是有道理的。

下面這個類,我們將用於建立四個顯式常量:


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

下面是透過這個類建立的常量:


1 Club, Diamond, Heart, Spade = Suit('Club','?'), Suit('Diamond','?'), Suit('Heart','?'), Suit('Spade','?')

現在我們可以透過下面展示的程式碼片段建立cards:


1 cards = [AceCard('A', Spade), NumberCard('2', Spade), NumberCard('3', Spade),]

這個小示例,這種方法對於單個特性的花色程式碼來說並不是一個巨大的進步。在更復雜的情況下,會有一些策略或狀態物件透過這個方式建立。透過從小的、靜態的常量物件中複用可以使策略或狀態設計模式更有效率。

我們必須承認,在Python中這些物件並不是技術上一成不變的,它是可變的。進行額外的編碼使得這些物件真正不變可能會有一些好處。

無關緊要的不變性

不變性很有吸引力但卻容易帶來麻煩。有時候被神話般的“惡意程式設計師”在他們的應用程式中透過修改常量值進行調整。從設計上考慮,這是非常愚蠢的。這些神話般的、惡意的程式設計師不會停止這樣做,因為已經沒有更好的方法去更簡潔簡單的在Python中編碼。惡意程式設計師訪問到原始碼並且修改它僅僅是希望儘可能輕鬆地編寫程式碼來修改一個常數。

在定義不可變物件的類的時候最好不要掙扎太久。


解釋2:與1基本相同,但有簡化


__init__()方法意義重大的原因有兩個。第一是在物件生命週期中初始化是最重要的一步;每個物件必須正確初始化後才能正常工作。第二是__init__()引數值可以有多種形式。

因為有很多種方式為__init__()提供引數值,所以對於物件建立有大量的使用案例,我們可以看看其中的幾個。我們想盡可能的弄清楚,因此我們需要定義一個初始化來正確的描述問題域。

在我們接觸__init__()方法之前,無論如何,我們都需要簡單粗略地看看Python中隱含的object類的層次結構。

在這一章,我們看看不同形式的簡單物件的初始化(例如:打牌)。在這之後,我們還可以看看更復雜的物件,就像包含集合的hands以及包含策略和狀態的players

隱式超類——object

每一個Python類都隱含了一個超類:object。它是一個非常簡單的類定義,幾乎不做任何事情。我們可以建立object的例項,但是我們不能用它做太多,因為許多特殊的方法容易丟擲異常。

當我們自定義一個類,object則為超類。下面是一個類定義示例,它使用新的名稱簡單的繼承了object

class X:    pass

下面是和自定義類的一些互動:

>>> X.__class__>>> X.__class__.__base__

我們可以看到該類是type類的一個物件,且它的基類為object

就像在每個方法中看到的那樣,我們也看看從object繼承的預設行為。在某些情況下,超類的特殊方法是我們想要的。而在其他情況下,我們又需要覆蓋這個特殊方法。

基類物件的__init__()方法

物件生命週期的基礎是它的建立、初始化和銷燬。我們將建立和銷燬推遲到後面章節的高階特殊方法中講,目前只關注初始化。

所有類的超類object,有一個預設包含pass__init__()方法,我們不需要去實現它。如果不實現它,則在物件建立後就不會建立例項變數。在某些情況下,這種預設行為是可以接受的。

我們總是給物件新增屬性,該物件為基類object的子類。思考下面的類,它需要兩個例項變數但不初始化它們:

class Rectangle:def area(self):return self.length * self.width

Rectangle類有一個使用兩個屬性來返回一個值的方法。這些屬性沒有初始化,是合法的Python程式碼。它可以明確地避免設定屬性,雖然感覺有點奇怪,但是合法。

下面是與Rectangle類的互動:

>>> r = Rectangle()>>> r.length, r.width = 13, 8>>> r.area()104

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

無論如何,這個設計給予了很大的靈活性,這樣有時候我們不用在__init__()方法中設定所有屬性。至此我們走的很順利。一個可選屬性其實就是一個子類,只是沒有真正的正式宣告為子類。我們建立多型在某種程度上可能會引起混亂,以及if語句的不恰當使用所造成的盤繞。雖然未初始化的屬性可能是有用的,但也很有可能是糟糕設計的前兆。

《Python之禪》中的建議:

"顯式比隱式更好。"

一個__init__()方法應該讓例項變數顯式。

非常差的多型

靈活和愚蠢就在一念之間。

當我們覺得需要像下面這樣寫的時候,我們正從靈活的邊緣走向愚蠢:

if 'x' in self.__dict__:

或者:

try:    self.xexcept AttributeError:

是時候重新考慮API並新增一個通用的方法或屬性。重構比新增if語句更明智。

在超類中實現__init__()

我們透過實現__init__()方法來初始化物件。當一個物件被建立,Python首先建立一個空物件併為該新物件呼叫__init__()方法。這個方法函式通常用來建立物件的例項變數並執行任何其他一次性處理。

下面是Card類示例定義的層次結構。我們將定義Card超類和三個子類,這三個子類是Card的變種。兩個例項變數直接由引數值設定,並透過初始化方法計算:


class Card:def __init__(self, rank, suit):self.suit = suitself.rank = rankself.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, 11class FaceCard(Card):def _points(self):return 10, 10

在這個示例中,我們提取__init__()方法到超類,這樣在Card超類中的通用初始化可以適用於三個子類NumberCardAceCardFaceCard

這是一種常見的多型設計。每一個子類都提供一個唯一的_points()方法實現。所有子類都有相同的簽名:有相同的方法和屬性。這三個子類的物件在一個應用程式中可以交替使用。

如果我們為花色使用簡單的字元,我們可以建立Card例項,如下所示:

cards = [AceCard('A', ''), NumberCard('2',''), NumberCard('3',''),]

我們在列表中列舉出一些牌的類、牌值和花色。從長遠來說,我們需要更的工廠函式來建立Card例項,用這個方法列舉52張牌無聊且容易出錯。在我們接觸工廠函式之前,我們看一些其他問題。

使用__init__()建立顯而易見的常量

可以給牌定義花色類。在二十一點中,花色無關緊要,簡單的字串就可以。

我們使用花色建構函式作為建立常量物件示例。在許多情況下,我們應用中小部分物件可以透過常量集合來定義。小部分的靜態物件可能是實現策略模式或狀態模式的一部分。

在某些情況下,我們會有一個在初始化或配置檔案中建立的常量物件池,或者我們可以基於命令列引數建立常量物件。我們會在第十六章《命令列處理》中獲取初始化設計和啟動設計的詳細資訊。

Python沒有簡單正式的機制來定義一個不可變物件,我們將在第三章《屬性訪問、特性和描述符》中看看保證不可變性的相關技術。在本示例中,花色不可變是有道理的。

下面這個類,我們將用於建立四個顯而易見的常量:

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

下面是透過這個類建立的常量:

Club, Diamond, Heart, Spade = Suit('Club',''), Suit('Diamond',''), Suit('Heart',''), Suit('Spade','')

現在我們可以透過下面展示的程式碼片段建立cards

cards = [AceCard('A', Spade), NumberCard('2', Spade), NumberCard('3', Spade),]

這個小示例的方法對於單個字元花色的程式碼來說並沒有多大改進。在更復雜的情況下,會透過這個方式建立一些策略或狀態物件。從小的靜態常量池中複用物件使得策略或狀態設計模式效率更高。

我們必須承認,在Python中這些物件並不是技術上一成不變的,它是可變的。進行額外的編碼使得這些物件真正不可變可能會有一些好處。

無關緊要的不變性

不變性很有吸引力但卻容易帶來麻煩。有時候神話般的“惡意程式設計師”在他們的應用程式中透過修改常量值進行調整。從設計上考慮,這是非常愚蠢的。這些神話般的、惡意的程式設計師不會停止這樣做。在Python中沒有更好的方法保證沒有白痴的程式碼。惡意程式設計師訪問到原始碼並且修改它僅僅是希望儘可能輕鬆地編寫程式碼來修改一個常數。

在定義不可變物件的類的時候最好不要掙扎太久。在第三章《屬性訪問、特性和描述符》中,我們將在有bug的程式中提供合適的診斷資訊來展示如何實現不變性。

透過工廠函式對 __init__() 加以利用

我們可以透過工廠函式來構建一副完整的撲克牌。這會比列舉所有52張撲克牌要好得多。在Python中,我們有如下兩種常見的工廠方法:

  • 定義一個函式,該函式會建立所需類的物件。

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

在Python中,類不是必須的。只有當相關的工廠非常複雜的時候才會顯現出優勢。Python的優勢就是當一個簡單的函式可以做的更好時我們決不強迫使用類層次結構。

雖然這是一本關於物件導向程式設計的書,但函式真是一個好東西。這是常見也是最地道的Python。

如果需要的話,我們總是可以重寫一個函式為適當的可呼叫物件,可以將一個可呼叫物件重構到我們的工廠類層次結構中。我們將在第五章《使用Callables和Contexts》中學習可呼叫物件。

一般,類定義的優點是透過繼承實現程式碼重用。工廠類的函式就是包裝一些目標類層次結構和複雜物件的構造。如果我們有一個工廠類,當擴充套件目標類層次結構的時候,我們可以新增子類到工廠類中。這給我們提供了多型工廠類,不同的工廠類定義具有相同的方法簽名,可以交替使用。

這個類級別的多型對於靜態編譯語言如Java或C++非常有用。編譯器可以解決類和方法生成程式碼的細節。

如果選擇的工廠定義不能重用任何程式碼,則類層次結構在Python中不會有任何幫助。我們可以簡單的使用具有相同簽名的函式。

以下是我們各種Card子類的工廠函式:

def card(rank, suit):if rank == 1:return AceCard('A', suit)elif 2 

這個函式透過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 

以下是當我們嘗試建立整副牌將會發生的事情:

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 

我們重寫了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 partialpart_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 selfdef 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類層次結構中的運作方式。然而,它確實改變了我們應用程式建立物件的方式。

在各個子類中實現__init__()

當我們看到建立Card物件的工廠函式,再看看Card類設計。我想我們可能要重構牌值轉換功能,因為這是Card類自身應該負責的內容。這會將初始化向下延伸到每個子類。

這需要共用的超類初始化以及特定的子類初始化。我們要謹遵Don't Repeat Yourself(DRY)原則來保持程式碼可以被克隆到每一個子類中。

下面的示例展示了每個子類初始化的職責:

class Card:passclass NumberCard(Card):def  __init__(self, rank, suit):self.suit = suitself.rank = str(rank)self.hard = self.soft = rankclass AceCard(Card):def  __init__(self, rank, suit):self.suit = suitself.rank = "A"self.hard, self.soft =  1, 11class FaceCard(Card):def  __init__(self, rank, suit):self.suit = suitself.rank = {11: 'J', 12: 'Q', 13: 'K'}[rank]self.hard = self.soft = 10

這仍是清晰的多型。然而,缺乏一個真正的共用初始化,會導致一些冗餘。缺點在於重複初始化suit,所以必須將其抽象到超類中。各子類的__init__()會對超類的__init__()做顯式的引用。

該版本的Card類有一個超類級別的初始化函式用於各子類,如下面程式碼片段所示:

class Card:def __init__(self, rank, suit, hard, soft):self.rank = rankself.suit = suitself.hard = hardself.soft = softclass 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__()函式。好處是簡化了我們的工廠函式,如下面程式碼片段所示:

def card10(rank, suit):if rank == 1:return AceCard(rank, suit)elif 2 

簡化工廠函式不應該是我們關注的焦點。不過我們從這可以看到一些變化,我們建立了比較複雜的__init__()函式,而對工廠函式卻有一些較小的改進。這是比較常見的權衡。

工廠函式封裝複雜性

在複雜的__init__()方法和工廠函式之間有個權衡。最好就是堅持更直接,更少程式設計師友好的__init__()方法,並將複雜性推給工廠函式。如果你想封裝複雜結構,工廠函式可以做的很好。

簡單複合物件

複合物件也可被稱為容器。我們來看一個簡單的複合物件:一副單獨的牌。這是一個基本的集合。事實上它是如此基本,以至於我們不用過多的花費心思,直接使用簡單的list做為一副牌。

在設計一個新類之前,我們需要問這個問題:使用一個簡單的list是否合適?

我們可以使用random.shuffle()來洗牌和使用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中類並不是一個硬性要求。

在前面的程式碼中,一副牌只有兩個簡單的用例和一個似乎並不夠簡化的類定義。它的優勢在於隱藏實現的細節,但細節是如此微不足道,揭露它們幾乎沒有任何意義。在本章中,我們的關注主要放在__init__()方法上,我們將看一些建立並初始化集合的設計。

設計一個物件集合,有以下三個總體設計策略:

  • 封裝:該設計模式是現有的集合的定義。這可能是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()

我們已經定義了Deck,內部集合是一個list物件。Deckpop()方法簡單的委託給封裝好的list物件。

然後我們可以透過下面這樣的程式碼建立一個Hand例項:

d = Deck()hand = [d.pop(), d.pop()]

一般來說,Facade設計模式或封裝好方法的類是簡單的被委託給底層實現類的。這個委託會變得冗長。對於一個複雜的集合,我們可以委託大量方法給封裝的物件。

2. 繼承集合類

封裝的另一種方法是繼承內建類。這樣做的優勢是沒有重新實現pop()方法,因為我們可以簡單地繼承它。

pop()的優點就是不用寫過多的程式碼就能建立類。在這個例子中,繼承list類的缺點是提供了一些我們不需要的函式。

下面是繼承內建listDeck定義:

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)

在某些情況下,為了擁有合適的類行為,我們的方法將必須顯式地使用超類。在下面的章節中我們將會看到其他相關示例。

我們利用超類的__init__()方法填充我們的list物件來初始化單副撲克牌,然後我們洗牌。pop()方法只是簡單從list繼承過來且工作完美。從list繼承的其他方法也能一起工作。

3. 更多的需求和另一種設計

在賭場中,牌通常從牌盒發出,裡面有半打喜憂參半的撲克牌。這個原因使得我們有必要建立自己版本的Deck,而不是簡單、純粹的使用list物件。

此外,牌盒裡的牌並不完全發完。相反,會插入標記牌。因為有標記牌,有些牌會被保留,而不是用來玩。

下面是包含多組52張牌的Deck定義:

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

在這裡,我們使用super().__init__()來構建一個空集合。然後,我們使用self.extend()新增多次52張牌。由於我們在這個類中沒有使用覆寫,所以我們可以使用super().extend()

我們還可以透過super().__init__(),使用更深層巢狀的生成器表示式執行整個任務。如下面程式碼片段所示:

(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類描述的一個例子,很適合模擬玩家策略:

class Hand:def __init__(self, dealer_card):self.dealer_card = dealer_cardself.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例項

d = 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__()方法會建立一個物件的完整例項。這是一個更復雜的容器,當你在建立一個包含內部其他物件集合的完整例項的時候。如果我們可以一步就能構建這個複合物件,它將是非常有幫助的。

逐步增加專案的方法和一步載入所有專案的方法是一樣的。

例如,我們可能有如下面的程式碼片段所示的類:

class Hand2:def __init__(self, dealer_card, *cards):self.dealer_card = dealer_cardself.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物件:

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

第二個示例使用*cards引數一步載入一序列的Card類:

d = Deck()h = Hand2(d.pop(), d.pop(), d.pop())

對於單元,在一個宣告中使用這種方式通常有助於構建複合物件。更重要的是,這種簡單、一步的計算來構建複合物件有利於下一部分的序列化技術。

參考:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4289/viewspace-2804218/,如需轉載,請註明出處,否則將追究法律責任。

相關文章