国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

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

yvonne / 1783人閱讀

摘要:同時,有多個類級別的靜態構造函數的方法。這個累贅,無論如何,是被傳遞到每個多帶帶的對象構造函數表達式中。我們可能只有幾個特定的擔憂,提供額外關鍵字參數給構造函數。

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

沒有__init__()的無狀態對象

下面這個示例,是一個簡化去掉了__init__()的類。這是一個常見的Strategy設計模式對象。策略對象插入到主對象來實現一種算法或者決策。它可能依賴主對象的數據,策略對象自身可能沒有任何數據。我們經常設計策略類來遵循Flyweight設計模式:我們避免在Strategy對象內部進行存儲。所有提供給Strategy的值都是作為方法的參數值。Strategy對象本身可以是無狀態的。這更多是為了方法函數的集合而非其他。

在本例中,我們為Player實例提供了游戲策略。下面是一個抓牌和減少其他賭注的策略示例(比較笨的策略):

class GameStrategy:
    def insurance(self, hand):
        return False
    def split(self, hand):
        return False
    def double(self, hand):
        return False
    def hit(self, hand):
        return sum(c.hard for c in hand.cards) <= 17

每個方法都需要當前的Hand作為參數值。決策是基于可用信息的,也就是指莊家的牌和閑家的牌。

我們可以使用不同的Player實例來構建單個策略實例,如下面代碼片段所示:

dumb = GameStrategy()

我們可以想象創造一組相關的策略類,在21點中玩家可以針對各種決策使用不同的規則。

一些額外的類定義

如前所述,一個玩家有兩個策略:一個用于下注,一個用于出牌。每個Player實例都與模擬計算執行器有一序列的交互。我們稱計算執行器為Table類。

Table類需要Player實例提供以下事件:

玩家必須基于下注策略來設置初始賭注。

玩家將得到一手牌。

如果手牌是可分離的,玩家必須決定是分離或不基于出牌策略。這可以創建額外的Hand實例。在一些賭場,額外的一手牌也是可分離的。

對于每個Hand實例,玩家必須基于出牌策略來決定是要牌、加倍或停牌。

玩家會獲得獎金,然后基于輸贏情況調整下注策略。

從這,我們可以看到Table類有許多API方法來獲得賭注,創建Hand對象提供分裂、分解每一手牌、付清賭注。這個對象跟蹤了一組Players的出牌狀態。

以下是處理賭注和牌的Table類:

class Table:
    def __init__(self):
        self.deck = Deck()
    def place_bet(self, amount):
        print("Bet", amount)
    def get_hand(self):
        try:
            self.hand = Hand2(d.pop(), d.pop(), d.pop())
            self.hole_card = d.pop()
        except IndexError:
            # Out of cards: need to shuffle.
            self.deck = Deck()
            return self.get_hand()
        print("Deal", self.hand)
        return self.hand
    def can_insure(self, hand):
        return hand.dealer_card.insure

Player使用Table類來接收賭注,創建一個Hand對象,出牌時根據這手牌來決定是否買保險。使用額外方法去獲取牌并決定償還。

get_hand()中展示的異常處理不是一個精確的賭場玩牌模型。這可能會導致微小的統計誤差。更精確的模擬需要編寫一副牌,當空的時候可以重新洗牌,而不是拋出異常。

為了正確地交互和模擬現實出牌,Player類需要一個下注策略。下注策略是一個有狀態的對象,決定了初始賭注。各種下注策略調整賭注通常都是基于游戲的輸贏。

理想情況下,我們渴望有一組下注策略對象。Python的裝飾器模塊允許我們創建一個抽象超類。一個非正式的方法創建策略對象引發的異常必須由子類實現。

我們定義了一個抽象超類,此外還有一個具體子類定義了固定下注策略,如下所示:

class BettingStrategy:
    def bet(self):
        raise NotImplementedError("No bet method")
    def record_win(self):
        pass
    def record_loss(self):
        pass

class Flat(BettingStrategy):
    def bet(self):
        return 1

超類定義了帶有默認值的方法。抽象超類中的基本bet()方法拋出異常。子類必須覆蓋bet()方法。其他方法可以提供默認值。這里給上一節的游戲策略添加了下注策略,我們可以看看Player類周圍更復雜的__init__()方法。

我們可以利用abc模塊正式化抽象超類的定義。就像下面的代碼片段那樣:

import abc
class BettingStrategy2(metaclass=abc.ABCMeta):
    @abstractmethod
    def bet(self):
        return 1
    def record_win(self):
        pass
    def record_loss(self):
?       pass

這樣做的優勢在于創建了BettingStrategy2的實例,不會造成任何子類bet()的失敗。如果我們試圖通過未實現的抽象方法來創建這個類的實例,它將引發一個異常來替代創建對象。

是的,抽象方法有一個實現。它可以通過super().bet()來訪問。

多策略的__init__()

我們可從各種來源創建對象。例如,我們可能需要復制一個對象作為創建備份或凍結一個對象的一部分,以便它可以作為字典的鍵或被置入集合中;這是內置類setfrozenset背后的想法。

有幾個總體設計模式,它們有多種方法來構建一個對象。一個設計模式就是一個復雜的__init__(),稱為多策略初始化。同時,有多個類級別的(靜態)構造函數的方法。

這些都是不兼容的方法。他們有完全不同的接口。

避免克隆方法

在Python中,一個克隆方法沒必要復制一個不需要的對象。使用克隆技術表明可能是未能理解Python中的面向對象設計原則。

克隆方法封裝了在錯誤的地方創建對象的常識。被克隆的源對象不能了解通過克隆建立的目標對象的結構。然而,如果源對象提供了一個合理的、得到了良好封裝的接口,反向(目標對象有源對象相關的內容)是可以接受的。

我們這里展示的例子是有效的克隆,因為它們很簡單。我們將在下一章展開它們。然而,展示這些基本技術是用來做更多的事情,而不是瑣碎的克隆,我們看看將可變對象Hand凍結為不可變對象。

下面可以通過兩種方式創建Hand對象的示例:

class Hand3:
    def __init__(self, *args, **kw):
      if len(args) == 1 and isinstance(args[0], Hand3):
          # Clone an existing hand; often a bad idea
          other = args[0]
          self.dealer_card = other.dealer_card
          self.cards = other.cards
      else:
          # Build a fresh, new hand.
          dealer_card, *cards = args
          self.dealer_card =  dealer_card
          self.cards = list(cards)

第一種情況,從現有的Hand3對象創建Hand3實例。第二種情況,從多帶帶的Card實例創建Hand3對象。

frozenset對象的相似之處在于可由多帶帶的項目或現有set對象創建。我們將在下一章學習創建不可變對象。使用像下面代碼片段這樣的構造,從現有的Hand創建一個新的Hand使得我們可以創建一個Hand對象的備份:

h = Hand(deck.pop(), deck.pop(), deck.pop())
memento = Hand(h)

我們保存Hand對象到memento變量中。這可以用來比較最后處理的牌與原來手牌,或者我們可以在集合或映射中使用時凍結它。

1. 更復雜的初始化選擇

為了編寫一個多策略初始化,我們經常被迫放棄特定的命名參數。這種設計的優點是靈活,但缺點是不透明的、毫無意義的參數命名。它需要大量的用例文檔來解釋變形。

我們還可以擴大我們的初始化來分裂Hand對象。分裂Hand對象的結果是只是另一個構造函數。下面的代碼片段說明了如何分裂Hand對象:

class Hand4:
    def __init__(self, *args, **kw):
        if len(args) == 1 and isinstance(args[0], Hand4):
            # Clone an existing handl often a bad idea
            other = args[0]
            self.dealer_card = other.dealer_card
            self.cards= other.cards
        elif len(args) == 2 and isinstance(args[0], Hand4) and "split" in kw:
            # Split an existing hand
            other, card = args
            self.dealer_card = other.dealer_card
            self.cards = [other.cards[kw["split"]], card]
        elif len(args) == 3:
            # Build a fresh, new hand.
            dealer_card, *cards = args
            self.dealer_card =  dealer_card
            self.cards = list(cards)
        else:
            raise TypeError("Invalid constructor args={0!r} kw={1!r}".format(args, kw))
    def __str__(self):
        return ", ".join(map(str, self.cards))

這個設計包括獲得額外的牌來建立合適的、分裂的手牌。當我們從一個Hand4對象創建一個Hand4對象,我們提供一個分裂的關鍵字參數,它從原Hand4對象使用Card類索引。

下面的代碼片段展示了我們如何使用被分裂的手牌:

d = Deck()
h = Hand4(d.pop(), d.pop(), d.pop())
s1 = Hand4(h, d.pop(), split=0)
s2 = Hand4(h, d.pop(), split=1)

我們創建了一個Hand4初始化的h實例并分裂到兩個其他Hand4實例,s1s2,并處理額外的Card類。21點的規則只允許最初的手牌有兩個牌值相等。

雖然這個__init__()方法相當復雜,它的優點是可以并行的方式從現有集創建fronzenset。缺點是它需要一個大文檔字符串來解釋這些變化。

2. 初始化靜態方法

當我們有多種方法來創建一個對象時,有時會更清晰的使用靜態方法來創建并返回實例,而不是復雜的__init__()方法。

也可以使用類方法作為替代初始化,但是有一個實實在在的優勢在于接收類作為參數的方法。在凍結或分裂Hand對象的情況下,我們可能需要創建兩個新的靜態方法凍結或分離對象。使用靜態方法作為代理構造函數是一個小小的語法變化,但當組織代碼的時候它擁有巨大的優勢。

下面是一個有靜態方法的Hand,可用于從現有的Hand實例構建新的Hand實例:

class Hand5:
    def __init__(self, dealer_card, *cards):
        self.dealer_card = dealer_card
        self.cards = list(cards)
    @staticmethod
    def freeze(other):
        hand = Hand5(other.dealer_card, *other.cards)
        return hand
    @staticmethod
    def split(other, card0, card1 ):
        hand0 = Hand5(other.dealer_card, other.cards[0], card0)
        hand1 = Hand5(other.dealer_card, other.cards[1], card1)
        return hand0, hand1
    def __str__(self):
        return ", ".join(map(str, self.cards))

一個方法凍結或創建一個備份。另一個方法分裂Hand5實例來創建兩個Hand5實例。

這更具可讀性并保存參數名的使用來解釋接口。

下面的代碼片段展示了我們如何通過這個版本分裂Hand5實例:

d = Deck()
h = Hand5(d.pop(), d.pop(), d.pop())
s1, s2 = Hand5.split(h, d.pop(), d.pop())

我們創建了一個初始的Hand5h實例,分裂成兩個手牌,s1和s2,處理每一個額外的Card類。split()靜態方法比__init__()簡單得多。然而,它不遵循從現有的set對象創建fronzenset對象的模式。

更多的__init__()技巧

我們會看看一些其他更高級的__init__()技巧。在前面的部分這些不是那么普遍有用的技術。

下面是Player類的定義,使用了兩個策略對象和table對象。這展示了一個看起來并不舒服的__init__()方法:

class Player:
    def __init__(self, table, bet_strategy, game_strategy):
        self.bet_strategy = bet_strategy
        self.game_strategy = game_strategy
        self.table = table
    def game(self):
        self.table.place_bet(self.bet_strategy.bet())
        self.hand = self.table.get_hand()
        if self.table.can_insure(self.hand):
            if self.game_strategy.insurance(self.hand):
                self.table.insure(self.bet_strategy.bet())
        # Yet more... Elided for now

Player__init__()方法似乎只是統計。只是簡單傳遞命名好的參數到相同命名的實例變量。如果我們有大量的參數,簡單地傳遞參數到內部變量會產生過多看似冗余的代碼。

我們可以如下使用Player類(和相關對象):

table = Table()
flat_bet = Flat()
dumb = GameStrategy()
p = Player(table, flat_bet, dumb)
p.game()

我們可以通過簡單的傳遞關鍵字參數值到內部實例變量來提供一個非常短的和非常靈活的初始化。

下面是使用關鍵字參數值構建Player類的示例:

class Player2:
    def __init__(self, **kw):
        """Must provide table, bet_strategy, game_strategy."""
        self.__dict__.update(kw)
    def game(self):
        self.table.place_bet(self.bet_strategy.bet())
        self.hand= self.table.get_hand()
        if self.table.can_insure(self.hand):
            if self.game_strategy.insurance(self.hand):
                self.table.insure(self.bet_strategy.bet())
        # etc.

為了簡潔而犧牲了大量可讀性。它跨越到一個潛在的默默無聞的領域。

因為__init__()方法減少到一行,它消除了某種程度上“累贅”的方法。這個累贅,無論如何,是被傳遞到每個多帶帶的對象構造函數表達式中。我們必須將關鍵字添加到對象初始化表達式中,因為我們不再使用位置參數,如下面代碼片段所示:

p2 = Player2(table=table, bet_strategy=flat_bet, game_strategy=dumb)

為什么這樣做呢?

它有一個潛在的優勢。這樣的類定義是相當易于擴展的。我們可能只有幾個特定的擔憂,提供額外關鍵字參數給構造函數。

下面是預期的用例:

>>> p1 = Player2(table=table, bet_strategy=flat_bet, game_strategy=dumb)
>>> p1.game()

下面是一個額外的用例:

>>> p2 = Player2(table=table, bet_strategy=flat_bet, game_strategy=dumb, log_name="Flat/Dumb")
>>> p2.game()

我們添加了一個與類定義無關的log_name屬性。也許,這可以被用作統計分析的一部分。Player2.log_name屬性可以用來注釋日志或其他數據的收集。

我們能添加的東西是有限的;我們只能添加沒有與內部使用的命名相沖突的參數。類實現的常識是需要的,用于創建沒有濫用已在使用的關鍵字的子類。由于**kw參數提供了很少的信息,我們需要仔細閱讀。在大多數情況下,比起檢查實現細節我們寧愿相信類是正常工作的。

在超類的定義中是可以做到基于關鍵字的初始化的,對于使用超類來實現子類會變得稍微的簡單些。我們可以避免編寫一個額外的__init__()方法到每個子類,當子類的唯一特性包括了簡單新實例變量。

這樣做的缺點是,我們已經模糊了沒有正式通過子類定義記錄的實例變量。如果只是一個小變量,整個子類可能有太多的編程開銷用于給一個類添加單個變量。然而,一個小變量常常會導致第二個、第三個。不久,我們將會認識到一個子類會比一個極其靈活的超類還要更智能。

我們可以(也應該)通過混合的位置和關鍵字實現生成這些,如下面的代碼片段所示:

class Player3(Player):
    def __init__(self, table, bet_strategy, game_strategy, **extras):
        self.bet_strategy = bet_strategy
        self.game_strategy = game_strategy
        self.table= table
        self.__dict__.update(extras)

這比完全開放定義更明智。我們已經取得了所需的位置參數。我們留下任何非必需參數作為關鍵字。這個闡明了__init__()給出的任何額外的關鍵字參數的使用。

這種靈活的關鍵字初始化取決于我們是否有相對透明的類定義。這種開放的態度面對改變需要注意避免調試名稱沖突,因為關鍵字參數名是開放式的。

1. 初始化類型驗證

類型驗證很少是一個合理的要求。在某種程度上,是沒有對Python完全理解。名義目標是驗證所有參數是否是一個合適的類型。試圖這樣做的原因主要是因為適當的定義往往是過于狹隘以至于沒有什么真正的用途。

這不同于確認對象滿足其他條件。數字范圍檢查,例如,防止無限循環的必要。

我們可以制造問題去試圖做些什么,就像下面__init__()方法中那樣:

class ValidPlayer:
    def __init__(self, table, bet_strategy, game_strategy):
        assert isinstance(table, Table)
        assert isinstance(bet_strategy, BettingStrategy)
        assert isinstance(game_strategy, GameStrategy)
        self.bet_strategy = bet_strategy
        self.game_strategy = game_strategy
        self.table = table

isinstance()方法檢查、規避Python的標準鴨子類型

我們寫一個賭場游戲模擬是為了嘗試不斷變化的GameStrategy。這些很簡單(僅僅四個方法),幾乎沒有從超類的繼承中得到任何幫助。我們可以獨立的定義缺乏整體的超類。

這個示例中所示的初始化錯誤檢查,將迫使我們通過錯誤檢查的創建子類。沒有可用的代碼是繼承自抽象超類。

最大的一個鴨子類型問題就圍繞數值類型。不同的數值類型將工作在不同的上下文中。試圖驗證類型的爭論可能會阻止一個完美合理的數值類型正常工作。當嘗試驗證時,我們有以下兩個選擇在Python中:

我們編寫驗證,這樣一個相對狹窄的集合類型是允許的,總有一天代碼會因為聰明的新類型被禁止而中斷。

我們避開驗證,這樣一個相對廣泛的集合類型是允許的,總有一天代碼會因為不聰明地類型被使用而中斷。

注意,兩個本質上是相同的。代碼可能有一天被中斷。要么因為禁止使用即使它是聰明,要么因為不聰明的使用。

讓它

一般來說,更好的Python風格就是簡單地允許使用任何類型的數據。

我們將在第4章《一致設計的基本知識》回到這個問題。

這個問題是:為什么限制未來潛在的用例?

通常回答是,沒有理由限制未來潛在的用例。

比起阻止一個聰明的,但可能是意料之外的用例,我們可以提供文檔、測試和調試日志幫助其他程序員理解任何可以處理的限制類型。我們必須提供文檔、日志和測試用例,這樣額外的工作開銷最小。

下面是一個示例文檔字符串,它提供了對類的預期:

class Player:
    def __init__(self, table, bet_strategy, game_strategy):
        """Creates a new player associated with a table,
            and configured with proper betting and play strategies
            :param table: an instance of :class:`Table`
            :param bet_strategy: an instance of :class:`BettingStrategy`
            :param  game_strategy: an instance of :class:`GameStrategy`
        """
        self.bet_strategy = bet_strategy
        self.game_strategy = game_strategy
        self.table = table

程序員使用這個類已經被警告了限制類型是什么。其他類型的使用是被允許的。如果類型不符合預期,執行會中斷。理想情況下,我們將使用unittestdoctest來發現bug。

2. 初始化、封裝和私有

一般Python關于私有的政策可以總結如下:我們都是成年人了。

面向對象的設計有顯式接口和實現之間的區別。這是封裝的結果。類封裝了數據結構、算法、一個外部接口或者一些有意義的事情。這個想法是從實現細節封裝分離基于類的接口。

但是,沒有編程語言反映了每一個設計細節。Python中,通常情況下,并沒有考慮都用顯式代碼實現所有設計。

類的設計,一方面是沒有完全在代碼中有私有(實現)和公有(接口)方法或屬性對象的區別。私有的概念主要來自(c++或Java)語言,這已經很復雜了。這些語言設置包括如私有、保護、和公有以及“未指定”,這是一種半專用的。私有關鍵字的使用不當,通常使得子類定義產生不必要的困難。

Python私有的概念很簡單,如下

本質上都是公有的。源代碼是可用的。我們都是成年人。沒有什么可以真正隱藏的。

一般來說,我們會把一些名字的方式公開。他們普遍實現細節,如有變更,恕不另行通知,但是沒有正式的私有的概念。

在部分Python中,命名以_開頭的一般是非公有的。help()函數通常忽略了這些方法。Sphinx等工具可以從文檔隱藏這些名字。

Python的內部命名是以__開始(結束)的。這就是Python保持內部不與應用程序的命名起沖突。這些內部的集合名稱完全是由語言內部參考定義的。此外,在我們的代碼中嘗試使用__試圖創建“超級私人”屬性或方法是沒有任何好處的。一旦Python的發行版本開始使用我們選擇內部使用的命名,會造成潛在的問題。同樣,我們使用這些命名很可能與內部命名發生沖突。

Python的命名規則如下:

大多數命名是公有的。

_開頭的都是非公有的。使用它們來實現細節是真正可能發生變化的。

__開頭或結尾的命名是Python內部的。我們不能這樣命名;我們使用語言參考定義的名稱。

一般情況下,Python方法使用文檔和好的命名來表達一個方法(或屬性)的意圖。通常,接口方法會有復雜的文檔,可能包括doctest的示例,而實現方法將有更多的簡寫文檔,很可能沒有doctest示例。

新手Python程序員,有時奇怪私有沒有得到更廣泛的使用。而經驗豐富的Python程序員,卻驚訝于為了整理并不實用的私有和公有聲明去消耗大腦的卡路里,因為從方法的命名和文檔中就能知道變量名的意圖。

總結

在本章中,我們回顧了__init__()方法的各種設計方案。在下一章,我們將看一看特別的以及一些高級的方法。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/45375.html

相關文章

  • [] Python 學習 —— __init__() 方法 3

    摘要:簡單復合對象復合對象也可被稱為容器。它難以序列化對象并像這樣初始化來重建。接口仍然會導致多種方法計算。還要注意一些不完全遵循點規則的方法功能。逐步增加項目的方法和一步加載所有項目的方法是一樣的。另一個方法就是之前那樣的類定義。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python 在各個子類中實現__init_...

    leanote 評論0 收藏0
  • [] Python 學習 —— __init__() 方法 2

    摘要:工廠類的函數就是包裝一些目標類層次結構和復雜對象的構造。連貫的工廠類接口在某些情況下,我們設計的類在方法使用上定義好了順序,按順序求方法的值很像函數。這個工廠類可以像下面這樣使用首先,我們創建一個工廠實例,然后我們使用那個實例創建實例。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python 通過工廠函數對 __init_...

    xiaotianyi 評論0 收藏0
  • [] Python 學習 —— __init__() 方法 1

    摘要:第一是在對象生命周期中初始化是最重要的一步每個對象必須正確初始化后才能正常工作。第二是參數值可以有多種形式。基類對象的方法對象生命周期的基礎是它的創建初始化和銷毀。在某些情況下,這種默認行為是可以接受的。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python __init__()方法意義重大的原因有兩個。第一是在對象生命...

    MobService 評論0 收藏0
  • []PEP 380--子生成器的語法

    摘要:提議以下的新的生成器語法將被允許在生成器的內部使用其中表達式作用于可迭代對象,從迭代器中提取元素。子迭代器而非生成器的語義被選擇成為生成器案例的合理泛化。建議如果關閉一個子迭代器時,引發了帶返回值的異常,則將該值從調用中返回給委托生成器。 導語: PEP(Python增強提案)幾乎是 Python 社區中最重要的文檔,它們提供了公告信息、指導流程、新功能的設計及使用說明等內容。對于學習...

    fevin 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<