天天看點

《Python面向對象程式設計指南》——1.5 通過工廠函數調用__init()__

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

我們可以使用工廠函數來完成所有card對象的建立,這比枚舉52張牌的方式好很多。在python中,實作工廠有兩種途徑。

定義一個函數,傳回不同類的對象。

定義一個類,包含了建立對象的方法。這是完整的工廠設計模式,正如設計模式書中提到的。在類似java這樣的語言裡,工廠類層次結構是必需的,因為語言本身不支援可以脫離類而單獨存在的函數。

在python裡,類定義不是必需的。僅當特别複雜的情形,工廠類才是不錯的選擇。python的優勢之一是,對于隻需要簡單地定義一個函數就能做到的事情沒必要去定義類層次結構。

《Python面向對象程式設計指南》——1.5 通過工廠函數調用__init()__

如果需要,我們總可以将函數重寫為合适的可調用對象。進行工廠模式設計時,也可以将可調用對象進一步重構為工廠類的層次結構。我們将在第5章“可調用對象和上下文的使用”中詳細介紹可調用對象。

從大體上來看,類定義的優勢是:可以通過繼承來使得代碼可以被更好地重用。工廠類封裝了類本身的層次結構以及對象建構的複雜過程。對于已有的工廠類,可以通過添加子類的方式來完成擴充,這樣就獲得了工廠類的多态設計,不同的工廠類名有相同的方法簽名并可以在調用時通過替換對象來改變具體實作。

這種類級别的多态機制對于類似java和c++這樣的編譯型語言來說是非常有用的,可以在編譯器在生成目标代碼時決定類和方法的實作細節。

如果可替代的工廠類并沒有重用任何代碼,那麼類層次結構在python中并沒有多大作用,完全可以使用函數來替代。

以下是用來生成card子類對象的一個工廠函數的例子。

這個函數通過傳入牌面值rank和花色值suit來建立card對象。這樣一來,建立對象的工作更簡便了。我們已經把創造對象的過程封裝在了單獨的工廠函數内,外界無需了解對象層次結構以及多态的工作細節就可以通過調用工廠函數來建立對象。

如下代碼示範了如何使用工廠函數來構造deck對象。

這段代碼枚舉了所有牌面值和花色的牌,完成了52張牌對象的建立。

這裡需要注意card()函數裡的if語句。并沒有使用一個catch-all else語句做一些其他步驟,而隻是單純地抛出了一個異常。像這樣的catch-all else語句的使用方式是有争議的。

一方面,else語句不能不做任何事情,因為這将隐藏微小的設計錯誤。另一方面,一些else語句的意圖已經很明顯了。

是以,避免模糊的else語句是非常重要的。

關于這一點,可以參照以下工廠函數的定義。

建立紙牌對象可以通過如下代碼實作。

這是最好的方式嗎?如果if條件更複雜些呢?

一些程式員可以很快了解這樣的if語句,而另一些則會糾結于是否要對if語句的邏輯做進一步劃分。

作為進階的python程式員,我們不應該把else語句的意圖留給讀者去推斷,條件語句的意圖應當是非常直接的。

《Python面向對象程式設計指南》——1.5 通過工廠函數調用__init()__

工廠方法card()中包括了兩個很常見的結構。

if-elif序列。

映射。

為了簡單化,重構将是更好的選擇。

我們總可以使用elif條件語句代替映射。(是的,總可以。反過來卻不行;把elif條件轉換為映射有時是有風險的。)

以下是沒有使用映射card工廠類的實作。

這裡重寫了card()工廠方法,将映射轉換為了elif語句。比起前一個版本,這個函數在實作上獲得了更好的一緻性。

在一些情形下,可以使用映射而非這樣的一個elif條件語句鍊。如果認為使用一個elif條件語句鍊是表達邏輯的唯一明智的方式,那麼很容易會發現,它看起來很複雜。對于簡單的情形,做同樣的事情采用映射完成的代碼可以更好地工作,而且代碼的可讀性也更強。

由于類是第1級别的對象,從rank參數映射到對象是很容易的事情。

這個card工廠類就是使用映射實作的版本。

我們把rank映射為對象,然後又把rank值和suit值作為參數傳入card構造函數來建立card執行個體。

也可以使用一個defaultdict類,然而比起簡單的靜态映射其實并沒有簡化多少。下例就是它的實作。

defaultdict類的預設構造函數必須是無參的。我們使用了一個lambda構造函數作為常量的封裝函數。這個函數有個很明顯的缺陷,缺少從1到a和13到k的映射。當試圖添加這段代碼邏輯時,就遇到了個問題。

我們需要修改映射邏輯,除了提供card子類,還需要提供rank對象的字元串結果。如何實作這兩部分的映射?有4種常見的方案。

可以建立兩個并行的映射。此處并不推薦這種做法,後面的章節會說明為什麼這樣做是不值得的。

可以映射為一個二進制組。當然,這個方案也有一些弊端。

可以映射為partial()函數。partial()函數是fun``ctools子產品的一個功能。

也可以考慮修改類定義來完成映射邏輯。在下一節裡會介紹如何在子類中重寫__init()__函數來完成這個方案。

對于每個方案我們會通過具體示例逐一示範。

1.并行映射

以下是此方案代碼的基本實作。

這樣是不值得的。這種實作方式帶來了映射鍵1、11、12和13的邏輯重複。重複是糟糕的,因為軟體更新後通常會帶來對并行結構多餘的維護成本。

《Python面向對象程式設計指南》——1.5 通過工廠函數調用__init()__

2.映射到一個牌面值的元組

以下代碼示範了如何映射到二進制組的基本實作。

這個方案看起來還不錯。并沒有太多代碼來完成特殊情形的處理。接下來我們會看到當需要修改card類層次結構時:添加一個card子類時,如何來修改和擴充。

從rank值映射為類對象是很少見的,而且兩個參數中隻有一個用于對象的初始化。從rank映射到一個相對簡單的類或函數對象,而不必提供目的不明确的參數,這才是明智的選擇。

3.partial函數設計

除了映射到二進制組函數和隻提供一個參數來執行個體化的方案外,我們還可以建立partial()函數。這個函數可以用來實作可選參數。我們會從functools庫中使用partial()函數建立一個帶有rank參數的部分類。

以下示範了如何建立從rank到partial()函數的映射來完成對象的初始化。

通過調用partial()函數然後指派給part<code>_</code>class,完成了與rank對象的關聯。可以使用同樣的方式建立suit對象,并完成最終card對象的建立。partial()函數的使用在函數式程式設計中是很常見的,當使用的是函數而非對象方法的時候就可以考慮使用。

大緻上,partial()函數在面向對象程式設計中不是很常用。我們可以簡單地提供構造函數的不同版本來做同樣的事情。partial()函數和構造對象時的流暢接口很類似。

4.工廠模式的流暢api設計

有時我們定義在類中的方法必須按特定的順序來調用。這種按順序調用的方法和建立partial()函數的方式非常類似。

假如有這樣的函數調用x.a().b()。對于x(a,b)這個函數,放在partial()函數的實作就可以是先調用x.a()再調用b()函數,這種方式可以了解為x(a)(b)。

這意味着python在管理狀态方面提供了兩種選擇。我們可以直接更新對象或者對具有狀态的對象使用partial()函數。由于兩種方式是等價的,因而可以把partial()函數重構為工廠對象建立的流暢接口。我們在流暢接口函數中設定可以回報self值的rank對象,然後傳入花色類進而建立card執行個體。

如下是card工廠流暢接口的定義,包含了兩個函數,它們必須按順序調用。

先是使用rank()函數更新了構造函數的狀态,然後通過suit()函數創造了最終的card對象。

這個工廠類可以以如下方式來使用。

我們先執行個體化一個工廠對象,然後再建立card執行個體。這種方式并沒有利用__init()__在card類層次結構中的作用,改變的是調用者建立對象的方式。