通俗 Python 設計模式——原型設計模式

平田發表於2016-10-31

從本文開始,將會寫一系列關於Python 設計模式通俗講解的文章,主要參考《Python 設計模式》一書,順序可能與原書目錄順序有所不同,因為我本身也是一邊學習一邊總結。本系列文章的主旨是剝開復雜的理論外殼,用最通俗的方法來講解Python 設計模式。並且,通過修改程式碼為更簡潔的形式,來更好的破除_**專業詞彙迷信**_。

《Python 設計模式》一書將各種設計模式做了一個簡單分類:

  • 建立型模式
  • 結構型模式
  • 行為型模式

今天要討論的是第一種——建立型模式——中的原型設計模式

根據書上的說法,原型模式的作用是:

當我們已有一個物件,並希望建立該物件的一個完整副本時,原型模式就派上用場了。在我們知道物件的某些部分會被變更但又希望保持原有物件不變之時,通常需要物件的一個副本。在這樣的案例中,重新建立原有物件是沒有意義的(請參考網頁[ http://t.cn/RqBrOuM ])。

另一個案例是,當我們想複製一個複雜物件時,使用原型模式會很方便。對於複製複雜物件,我們可以將物件當作是從資料庫中獲取的,並引用其他一些也是從資料庫中獲取的物件。若通過多次重複查詢資料來建立一個物件,則要做很多工作。在這種場景下使用原型模式要方便得多。

這一段描述太抽象,其實用最簡單的話來說就是:

第一種情況,你有一個ApplePen,想要一個一模一樣的ApplePen,可以直接製作一個ApplePen的副本。

第二種情況,你有一個ApplePen,想要一個PineapplePen,可以通過製作一個ApplePen的副本,然後修改這個副本為PineapplePen的方式來達成目的。

第三種情況,你有一個非常複雜PenPineappleApplePen,然後你需要一個PineapplePenApplePen,多次操作可能會非常麻煩,那麼可以以PenPineappleApplePen為原型,建立一個副本PenPineappleApplePen,再通過簡單的幾步就能將其修改為PineapplePenApplePen。(其實一般情況下與第二種情況沒差)

看起來好像還是很複雜?無所謂,把每種情況中,看不懂的 | 第一個 | 多次出現的 | 單詞改為A,看不懂的 | 第二個 | 多次出現的 | 單詞改為B,就明白了。

好,我們現在把概念用最簡單的話梳理清楚了,那麼我們來看看程式碼實現。書上給出了這樣一個例子:

一本書,第一版出版了。10年後,第二版出版了,有一定的修改。如何使用原型模式建立一個展示圖書資訊的應用?

這裡其實可以看做我們總結出來的第二種情況,即:在有了原型物件的情況下,如何通過使用原型設計模式的編碼,來得到新的物件。

我們先看看書上給出的示例程式碼,原書程式碼中沒有註釋,我嘗試補充了一些註釋,也許會有一定的錯漏:

import copy
from collections import OrderedDict

class Book:
    def __init__(self, name, authors, price, **rest):
        '''rest的例子有:出版商、長度、標籤、出版日期'''
        self.name = name
        self.authors = authors
        self.price = price
        self.__dict__.update(rest)      # 新增其他額外屬性

    def __str__(self):
        mylist = []
        ordered = OrderedDict(sorted(self.__dict__.items()))
        for i in ordered.keys():
            mylist.append('{}: {}'.format(i, ordered[i]))
            if i == 'price':
                mylist.append('$')
            mylist.append('\n')
        return ''.join(mylist)

class Prototype:
    def __init__(self):
        self.objects = dict()    # 初始化一個原型列表

    def register(self, identifier, obj):
        # 在原型列表中註冊原型物件
        self.objects[identifier] = obj

    def unregister(self, identifier):
        # 從原型列表中刪除原型物件
        del self.objects[identifier]

    def clone(self, identifier, **attr):
        # 根據 identifier 在圓形列表中查詢原型物件並克隆
        found = self.objects.get(identifier)
        if not found:
            raise ValueError('Incorrect object identifier: {}'.format(identifier))
        obj = copy.deepcopy(found)
        obj.__dict__.update(attr)   # 用新的屬性值替換原型物件中的對應屬性
        return obj

def main():
    b1 = Book('The C Programming Language', ('Brian W. Kernighan', 'Dennis M.Ritchie'),
    price=118, publisher='Prentice Hall', length=228, publication_date='1978-02-22',
    tags=('C', 'programming', 'algorithms', 'data structures'))

    prototype = Prototype()
    cid = 'k&r-first'
    prototype.register(cid, b1)
    b2 = prototype.clone(cid, name='The C Programming Language(ANSI)', price=48.99, length=274, publication_date='1988-04-01', edition=2)

    for i in (b1, b2):
        print(i)
    print("ID b1 : {} != ID b2 : {}".format(id(b1), id(b2)))

if __name__ == '__main__':
    main()

我們將這段程式碼儲存為prototype.py,在控制檯執行,可以得到如下輸出:

authors: ('Brian W. Kernighan', 'Dennis M.Ritchie')
length: 228
name: The C Programming Language
price: 118$
publication_date: 1978-02-22
publisher: Prentice Hall
tags: ('C', 'programming', 'algorithms', 'data structures')

authors: ('Brian W. Kernighan', 'Dennis M.Ritchie')
edition: 2
length: 274
name: The C Programming Language(ANSI)
price: 48.99$

publication_date: 1988-04-01
publisher: Prentice Hall
tags: ('C', 'programming', 'algorithms', 'data structures')

ID b1 : 2378797084512 != ID b2 : 2378796684008

熟悉 Python 的朋友可以發現,其實這段程式碼在 Python 中,我們要實現一樣的效果,並沒有這麼複雜,完全可以不使用這樣的方法,我們更熟悉的方法是這樣的,這裡只修改main函式:

def main():
    b1 = Book('The C Programming Language', 
              ('Brian W. Kernighan', 'Dennis M.Ritchie'),
              price=118,
              publisher='Prentice Hall',
              length=228,
              publication_date='1978-02-22',
              tags=('C', 'programming', 'algorithms', 'data structures'))

    # 這裡我們徹底拋棄之前的原型設計模式的寫法

    b2 = copy.deepcopy(b1)
    b2.name = 'The C Programming Language(ANSI)'
    b2.price = 48.99
    b2.length = 274
    b2.publication_date = '1988-04-01'
    b2.edition = 2

    for i in (b1, b2):
        print(i)
    print("ID b1 : {} != ID b2 : {}".format(id(b1), id(b2)))

同樣的內容,替換後再次執行,輸出結果幾乎一模一樣。所以,我們現在明白了,得益於 Python 的靈活性,其實在一般情況下沒必要搞得那麼費勁,用最簡單粗暴的方式就可以達成我們想要的目標。當然,這裡並不是說原型模式就沒用,各位根據情況,靈活處理就好。

相關文章