用Python講解原型設計模式

  • 原型模式
    有時,我們需要原原本本地爲對象創建一個副本。舉例來說,假設你想創建一個應用來存儲、分享、編輯(比如,修改、添加註釋及刪除)食譜。用戶Bob找到一份蛋糕食譜,在做了一些改變後,覺得自己做的蛋糕非常美味,想要與朋友Alice分享這個食譜。但是該如何分享食譜呢? 如果在與Alice分享之後,Bob想對食譜做進一步的試驗,Alice手裏的食譜也能跟着變化嗎? Bob能夠持有蛋糕食譜的兩個副本嗎? 對蛋糕食譜進行的試驗性變更不應該對原本美味蛋糕的食譜造成影響。

這樣的問題可以通過讓用戶對同一份食譜持有多個獨立的副本來解決。每個副本被稱爲一個克隆,是某個時間點原有對象的一個完全副本。這裏時間是一個重要因素。因爲它會影響克隆所包含的內容。例如,如果Bob在對蛋糕食譜做改進以臻完美之前就與Alice分享了,那麼Alice就絕不可能像Bob那樣烘烤出自己的美味蛋糕,只能按照Bob原來找到的食譜烘烤蛋糕。

注意引用與副本之間的區別。如果Bob和Alice持有的是同一個蛋糕食譜對象的兩個引用,那麼Bob對食譜做的任何改變,對於Alice的食譜版本都是可見的,反之亦然。我們想要的是Bob和Alice各自持有自己的副本,這樣他們可以各自做變更而不會影響對方的食譜。實際上Bob需要蛋糕食譜的兩個副本: 美味版本和試驗版本。

圖略,其實就是引用和複製的差別。很簡單。

下面是一個關於原型模式的示例,來自github:

#!/usr/bin/env python

-- coding: utf-8 --

import copy

class Prototype:

value = 'default'

def clone(self, **attrs):
    """Clone a prototype and update inner attributes dictionary"""
    obj = copy.deepcopy(self)
    obj.__dict__.update(attrs)
    return obj

class PrototypeDispatcher:

def __init__(self):
    self._objects = {}

def get_objects(self):
    """Get all objects"""
    return self._objects

def register_object(self, name, obj):
    """Register an object"""
    self._objects[name] = obj

def unregister_object(self, name):
    """Unregister an object"""
    del self._objects[name]

def main():
dispatcher = PrototypeDispatcher()
prototype = Prototype()

d = prototype.clone()
a = prototype.clone(value='a-value', category='a')
b = prototype.clone(value='b-value', is_checked=True)
dispatcher.register_object('objecta', a)
dispatcher.register_object('objectb', b)
dispatcher.register_object('default', d)
print([{n: p.value} for n, p in dispatcher.get_objects().items()])

if name == 'main':
main()

OUTPUT

[{'objectb': 'b-value'}, {'default': 'default'}, {'objecta': 'a-value'}]

import copy

class Prototype:
def init(self):
self._objects = {}

def register_object(self, name, obj):
    """Register an object"""
    self._objects[name] = obj

def unregister_object(self, name):
    """Unregister an object"""
    del self._objects[name]

def clone(self, name, **attr):
    """Clone a registered object and update inner attributes dictionary"""
    obj = copy.deepcopy(self._objects.get(name))
    obj.__dict__.update(attr)
    return obj

def main():
class A:
pass

a = A()
prototype = Prototype()
prototype.register_object('a', a)
b = prototype.clone('a', a=1, b=2, c=3)

print(a)
print(b.a, b.b, b.c)

if name == 'main':
main()

運行得到的結果爲:

<main.main.<locals>.A object at 0x1066e3cc0>
1 2 3
原型設計模式(Prototype design pattern)幫助我們創建對象的克隆,其最簡單的形式就是一個clone()函數,接受一個對象作爲輸入參數,返回輸入對象的一個副本。在Python中,這可以使用copy.deepcopy()函數來完成。來看一個例子,下面的代碼中(文件clone.py)有兩個類,A和B。A是父類,B是衍生類/子類。在主程序部分,我們創建一個類B的實例b,並使用deepcopy()創建b的一個克隆c。結果是所有成員都被複制到了克隆c,以下是代碼演示。作爲一個有趣的練習,你也可以嘗試在對象的組合形式上使用deepcopy()。

左側是兩個引用。Bob和Alice參考的是同一個食譜,這本質上意味着兩者共享食譜,並且所有變更兩人皆可見。右側是同一個食譜的兩個不同副本,這樣允許各自獨立地變更,Alice做的改變不會影響Bob做的改變,反之亦然。

import copy

class A:
def init(self):
self.x = 18
self.msg = 'Hello'

class B(A):
def init(self):
A.init(self)
self.y = 34
def str(self):
return '{}, {}, {}'.format(self.x, self.msg, self.y)

if name == 'main':
b = B()
c = copy.deepcopy(b)
print([str(i) for i in (b, c)])
print([i for i in (b, c)])
得到的輸出爲:

['18, Hello, 34', '18, Hello, 34']
[<main.B object at 0x1066f2668>, <main.B object at 0x1066f26a0>]
重要的是注意兩個對象位於兩個不同的內存地址(輸出中的0x…部分)。這意味着兩個對象是兩個獨立的副本。

在本章稍後的3.4節中,我們將看到爲了保持一個被克隆對象的註冊表,如何將copy.deepcopy與封裝在某個類中的一些額外的樣板代碼一起使用。

現實生活中的示例
原型設計模式無非就是克隆一個對象。有絲分裂,即細胞分裂的過程,是生物克隆的一個例子。在這個過程中,細胞核分裂產生兩個新的細胞核,其中每個都有與原來細胞完全相同的染色體和DNA內容(請參考網頁[t.cn/RqBrOuM])。

  • 軟件中的示例
    很多Python應用都使用了原型模式(請參考網頁[t.cn/RqBruae]),但幾乎都不稱之爲原型模式,因爲對象克隆是編程語言的一個內置特性。

可視化工具套件(Visualization Toolkit,VTK)(請參考網頁[t.cn/hCDIf])是原型模式的一個應用。VTK是一個開源的跨平臺系統,用於三維計算機圖形/圖片處理以及可視化。VTK使用原型模式來創建幾何元素(比如,點、線、六面體等,請參考網頁[t.cn/RqBrecy])的克隆。

另一個使用原型模式的項目是music21。根據該項目頁面所述,“music21是一組工具,幫助學者和其他積極的聽衆快速簡便地得到音樂相關問題的答案”(請參考網頁[t.cn/RGK8f1V])。 music21工具套件使用原型模式來複制音符和總譜(請參考網頁[t.cn/RqBdhGK])。

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

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

至此,我們僅涉及了引用與副本的問題,而副本又可以進一步分爲 深副本與淺副本。深副本就是我們在本章中到目前爲止所看到的:原始對象的所有數據都被簡單地複製到克隆對象中,沒有例外。淺副本則依賴引用。我們可以引入數據共享和寫時複製一類的技術來優化性能(例如,減小克隆對象的創建時間)和內存使用。如果可用資源有限(例如,嵌入式系統)或性能至關重要(例如,高性能計算),那麼使用淺副本可能更佳。

在Python中,可以使用copy.copy()函數進行淺複製。以下內容引用自Python官方文檔,說明了淺副本(copy.copy())和深副本(copy.deepcopy())之間的區別(請參考網頁[t.cn/RqBdSj1])。

淺副本構造一個新的複合對象後,(會儘可能地)將在原始對象中找到的對象的引用插入新對象中。
深副本構造一個新的複合對象後,會遞歸地將在原始對象中找到的對象的副本插入新對象中。
你能想到什麼使用淺副本比深副本更好的例子嗎?

  • 實現
    編程方面的書籍歷經多個版本的情況並不多見。例如,Kernighan和Ritchie編著的C編程經典教材《C程序設計語言》(The C Programming Language)歷經兩個版本。第一版1978年出版,那時C語言還沒被標準化。該書第二版10年後纔出版,涵蓋C語言標準(ANSI)版本。這兩個版本之間的區別是什麼?列舉幾個:價格、長度(頁數)以及出版日期。但也有很多相似之處:作者、出版商以及描述該書的標籤/關鍵詞都是完全一樣的。這表明從頭創建一版新書並不總是最佳方式。如果知道兩個版本之間的諸多相似之處,則可以先克隆一份,然後僅修改新版本與舊版本之間的不同之處。

來看看可以如何使用原型模式創建一個展示圖書信息的應用。我們以一本書的描述開始。除了常規的初始化之外,Book類展示了一種有趣的技術可避免可伸縮構造器問題。在init() 方法中,僅有三個形參是固定的:name、authors和price,但是使用rest變長列表,調用者能以關鍵詞的形式(名稱=值)傳入更多的參數。self.dict.update(rest)一行將rest的內容添加到Book類的內部字典中,成爲它的一部分。

但這裏有個問題。我們並不知道所有被添加參數的名稱,但又需要訪問內部字典將這些參數應用到str()中,並且字典的內容並不遵循任何特定的順序,所以使用一個OrderedDict來強制元素有序,否則,每次程序執行都會產生不同的輸出。當然,你不應該把我的話當作理所當然。作爲一個練習,嘗試刪除使用的OrderedDict和sorted(),運行一下示例代碼看看我說得對不對。

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)

Prototype類實現了原型設計模式。Prototype類的核心是clone()方法,該方法使用我們熟悉的copy.deepcopy()函數來完成真正的克隆工作。但Prototype類在支持克隆之外做了一點更多的事情,它包含了方法register()和unregister(),這兩個方法用於在一個字典中追蹤被克隆的對象。注意這僅是一個方便之舉,並非必需。

此外,clone()方法和Book類中的str使用了相同的技巧,但這次是因爲別的原因。使用變長列表attr,我們可以僅傳遞那些在克隆一個對象時真正需要變更的屬性變量,如下所示。

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):
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
main()函數以實踐的方式展示了本節開頭提到的《C程序設計語言》一書克隆的例子。克隆該書的第一個版本來創建第二個版本,我們僅需要傳遞已有參數中被修改參數的值,但也可以傳遞額外的參數。在這個案例中,edition就是一個新參數,在書的第一個版本中並不需要,但對於克隆版本卻是很有用的信息。

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)))
注意,代碼中使用了函數id()返回對象的內存地址。當使用深副本來克隆一個對象時,克隆對象的內存地址必定與原始對象的內存地址不一樣。文件prototype.py的完整內容如下所示。

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

main()
得到的輸出爲:

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 : 4402992184 != ID b2 : 4402992016
id()的輸出依賴計算機當前的內存分配情況,該輸出在每次執行這個程序時都應該是不一樣的。無論實際的內存地址是多少,原始對象與克隆對象的內存地址都絕無可能相同。

原型模式確實按預期生效了。《C程序設計語言》的第二版複用了第一版設置的所有信息,所有我們定義的不同之處僅應用於第二版,第一版不受影響。看到id()函數的輸出顯示兩個內存地址不相同,我們就更加確信了。

作爲練習,你可以提出自己的原型模式的例子。以下是一些想法。

本章提到的食譜例子
本章提到的從數據庫獲取數據填充對象的例子
複製一張圖片,這樣你可以做些自己的修改而不會影響原來的圖片
小結
在本章中,我們學習瞭如何使用原型設計模式。原型模式用於創建對象的完全副本。確切地說,創建一個對象的副本可以指代以下兩件事情。

當創建一個淺副本時,副本依賴引用
當創建一個深副本時,副本複製所有東西
第一種情況中,我們關注提升應用性能和優化內存使用,在對象之間引入數據共享,但需要小心地修改數據,因爲所有變更對所有副本都是可見的。淺副本在本章中沒有過多介紹,但也許你會想試驗一下。

第二種情況中,我們希望能夠對一個副本進行更改而不會影響其他對象。對於我們之前看到的蛋糕食譜示例這類案例,這一特性是很有用的。這裏不會進行數據共享,所以需要關注因對象克隆而引入的資源耗用問題。

我們展示了一個深副本的簡單示例,在Python中,深副本使用copy.deepcopy()函數來完成。我們也提到一些現實生活中發現的克隆例子,並着重講述了有絲分裂的例子。

許多軟件項目都使用了原型模式,但因爲在Python中這是一個內置特性,所以並不稱之爲原型模式。這些軟件項目包括VTK(它使用原型模式創建幾何學元素的克隆)和music21(它使用 原型模式複製樂譜和音符)。

最後,我們討論了原型模式的應用案例,並實現一個程序以支持克隆書籍對象。這樣在新版本中所有信息無需改變即可複用,並且同時可以更新變更的信息,並添加新的信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章