天天看點

《Python面向對象程式設計指南》——1.12 更多的__init__()技術

本節書摘來自異步社群《python面向對象程式設計指南》一書中的第1章,第1.12節,作者[美]steven f. lott, 張心韬 蘭亮 譯,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

我們再來看一下其他一些更進階的__init__()技術的應用。相比前面的介紹,它們的應用場景不是特别常見。

以下是player類的定義,初始化使用了兩個政策對象和一個table對象。這個__init__()函數看起來不夠漂亮。

player類中的__init__()函數的行為似乎僅僅是儲存對象。代碼邏輯隻是把參數的值複制到同樣名稱的變量中。如果我們有很多參數,複制邏輯會顯得臃腫且重複。

我們可以像如下代碼這樣使用這個player類(和相關對象)。

我們可以通過把關鍵字參數值直接轉換為内部變量,以提供一個非常短而且靈活的初始化方式。

以下是一種使用關鍵字參數值來建立player類的方式。

為了換來簡潔的代碼,這種實作方式犧牲了大量的可讀性。它使得代碼意圖變得模糊。

既然__init__()函數縮減到了一行,函數的很多多餘的重複邏輯也被拿掉了。然而這種多餘也被轉化為對象各自的構造函數表達式。既然我們不再使用位置參數,那麼我們就需要為對象初始化表達式提供參數名,如以下代碼段所示。

為什麼這樣做?

這樣的類設計非常容易擴充,我們幾乎不用擔心是否需要傳入額外的參數給構造函數。

以下是調用的例子。

以下代碼示範了這個設計帶來的可擴充性。

我們添加了一個log_name屬性而并不需要修改類定義,這個屬性或許可以用來進行統計分析。player2.log_name屬性可以用于日志的注解或其他資料。

這裡存在一個限制,我們隻可以添加類内部不會發生沖突的參數名。在建立子類時需要了解類的實作,以避免關鍵字參數名沖突。由于**kw參數提供了很少的資訊,我們不得不去知道它的實作細節。可在大多數情況下,我們要相信一個類并使用它而不是去檢視它的實作細節。

這種基于關鍵字的初始化可以放在基類中實作,以簡化子類。當新需求導緻需要添加參數時,我們不必在每個子類中都實作一個__init__()函數。

這種實作方式的弊端在于存在一些變量在子類中沒有提供文檔說明。當僅需要添加一個變量時,可能需要改變整個類層次結構。第1個變量添加之後往往還會需要第2個和第3個。在設計的開始,我們應當考慮設計一些靈活的子類,而不是完美的基類。

我們可以(而且應該)像下面代碼段那樣同時使用位置變量和關鍵字變量。

這種方式看起來比完全開放的定義更明智。我們把必需的參數設為位置參數,把可選參數通過關鍵字參數傳入。這也示範了如何通過 extra關鍵字參數把可選參數傳入__init__()函數。

這樣的靈活性,基于關鍵字的初始化依賴于我們是否已經定義了相對透明的類。這種實作需要特别關注一下命名,因為關鍵字參數名是開放式的,要避免調試過程中發生命名沖突。

需要類型驗證的場景很少。從某種程度上說,這是對python的誤解。從概念上來看,類型驗證是為了驗證所有的參數類型是恰當的類型,而這裡對“恰當”的定義往往作用不大。

這和驗證對象是否符合其他标準是不同的,例如數字範圍檢查和防止無限循環。

在__init__()函數中實作以下邏輯可能會帶來問題。

這裡使用了isinstance()函數檢查了每個類型的合法性。

我們編寫了玩牌遊戲模拟器并通過不斷地改變gamestrategy類來進行實驗。由于它們都很簡單(隻有4個函數),繼承的好處不夠凸顯,我們可以單獨定義每個子類而不再定義基類。

正如本例中所示範的,我們将不得不建立子類,目的隻是為了通過初始化過程的錯誤檢查,而未能從抽象基類繼承到任何可用的代碼。

其中一個最大的鴨子類型問題是關于數值類型的,不同的數值類型會在不同的上下文工作。試圖驗證參數類型也許會導緻原本工作很好的一個數值類型不再工作。當試圖驗證時,在python中我們有以下兩種選擇。

為不是很廣泛的集合類型加驗證。一旦代碼不工作了我們将會知道本該允許使用的類型被禁止了。

針對相對廣泛的集合類型,通常不考慮加驗證,一旦代碼不工作了我們将會知道我們使用了一個不允許使用的類型。

這兩點基本表達了相同的意思。代碼某一天可能會無效,要麼是因為一個本該允許的類型被禁止了,要麼是使用了被禁止的類型。

《Python面向對象程式設計指南》——1.12 更多的__init__()技術

面臨這樣一個問題:為什麼要限制未來潛在的使用場景?

而通常沒有一個合理的理由來說明這一點。

為了不為以後的應用場景帶來阻礙,可以考慮提供文檔、測試和調試日志來幫助其他程式員了解哪些類型限制是可以被處理的。為了使工作量最小化,無論如何我們都必須提供文檔、日志和測試用例。

以下是一段示例文檔,用于說明類所需的參數。

當使用這個類時,就會從文檔得知類的參數需求。可以傳入任何類型。如果類型和期望的類型不相容,那麼代碼将會不工作。理想情況下,我們會使用文檔測試(doctest)和單元測試(unittest)來發現這些異常的場景。

關于python中的私有化可以概括為:大家都是成年人。

面向對象設計使得接口和實作有了很大的差别,這也是封裝的意義。一個類封裝了一種資料結構、一個算法和一個外部接口等,程式設計的目的是要把接口與實作分離。

然而,沒有程式設計語言會暴露出所有設計的細節。對于python,也是如此。

關于類設計的一個方面,這一點沒有用代碼示範:對象中有關私有(實作)和公有(接口)函數或屬性的差異。有些程式設計語言隻是在概念上支援私有(c++或java是兩個例子)已經很複雜了。這類語言中的通路修飾符包括了私有、保護、公有和“未指定”,可以了解為半私有。私有關鍵字經常被錯誤使用,為子類的定義帶來了沒必要的複雜性。

python中私有的概念很簡單,如下所示。

基本都是公有。源代碼随時可修改,大家都是成年人,沒有什麼是可以真正被隐藏的。

傳統上,我們會使用命名來表明哪些不是完全公有的。它們通常是容易變化的具體實作細節,然而并不存在正式的、概念上的私有。

python中的部分函數以_命名,标記為不完全公有。help()函數通常會忽略這類函數。可以使用像sphinx這樣的工具從文檔中查找出它們的命名。

python的内部命名以__起始(和結尾)。這也是python如何避免内部和外部應用程式發生沖突的方式。這些内部集合的命名方式完全隻是參考。畢竟,沒有必要在代碼中試圖使用__字首來定義一個“超級私有”的屬性或函數。如果這樣做的話就為以後制造了一個潛在的麻煩,當新版本的python釋出并使用了同樣命名的函數或屬性時,就會有命名沖突。我們還有可能和新版本中的其他名稱發生沖突。

python中關于可見度的命名規則如下所示。

大部分名稱是公有的。

以_開始的名字通常不完全公有。使用它們來命名那些經常變化的函數,這些函數通常是實作細節。

以__作為字首和字尾的函數通常是python内部的。程式中不該使用;命名要參考程式設計語言的定義。

通常,python中的命名是根據函數(或屬性)的目的來定義的,并提供文檔說明。通常接口函數會有說明文檔以及文檔測試的例子,而實作細節的函數就不必了,提供簡單的說明就可以了。

對于剛接觸python的程式員,有時會對私有化不是很常用而感到驚訝。可對于已經熟悉python的程式員也會同樣驚訝于,為了不必要的私有和公有定義的順序而浪費很多腦細胞。因為函數名和文檔已經把意圖描述的很明白了。