1. 程式人生 > 實用技巧 >python設計模式之模板模式

python設計模式之模板模式

python設計模式之模板模式

編寫優秀程式碼的一個要素是避免冗餘。在面向物件程式設計中,方法和函式是我們用來避免編寫冗餘程式碼的重要工具。

現實中,我們沒法始終寫出100%通用的程式碼。許多演算法都有一些(但並非全部)通用步驟。廣度優先搜尋( Breadth-First Search, BFS)和深度優先搜尋( Depth-First Search, DFS)是其中不錯的例子,這兩個流行的演算法應用於圖搜尋問題。函式bfs()和dfs()在start和end之間存在一條路徑時返回一個元組(True, path);如果路徑不存在,則返回(False, path)(此時,path為空)。

def bfs(graph, start, end):
    path = []
    visited = [start]
    while visited:
        current = visited.pop(0)
        if current not in path:
        	path.append(current)
            if current == end:
                print(path)
                return (True, path)
            # 兩個頂點不相連,則跳過
            if current not in graph:
                continue
        visited = visited + graph[current]
    return (False, path)
def dfs(graph, start, end):
    path = []
    visited = [start]
    while visited:
        current = visited.pop(0)
        if current not in path:
            path.append(current)
            if current == end:
            	print(path)
                return (True, path)
            # 兩個頂點不相連,則跳過
            if current not in graph:
            	continue
            visited = graph[current] + visited
    return (False, path)	

注意兩個演算法之間的相似點。僅有一處不同(已加粗),其餘部分完全相同。稍後我們再回來討論這個問題。

可以使用列表的字典結構來表示這個有向圖。每個城市是字典中的一個鍵,列表的內容是從該城市始發的所有可能目的地。葉子頂點的城市(例如, Erfurt)使用一個空列表即可(無目的地)。

def main():
    graph = {
        'Frankfurt': ['Mannheim', 'Wurzburg', 'Kassel'],
        'Mannheim': ['Karlsruhe'],
        'Karlsruhe': ['Augsburg'],
        'Augsburg': ['Munchen'],
        'Wurzburg': ['Erfurt', 'Nurnberg'],
        'Nurnberg': ['Stuttgart', 'Munchen'],
        'Kassel': ['Munchen'],
        'Erfurt': [],
        'Stuttgart': [],
        'Munchen': []
    }
    bfs_path = bfs(graph, 'Frankfurt', 'Nurnberg')
    dfs_path = dfs(graph, 'Frankfurt', 'Nurnberg')
    print('bfs Frankfurt-Nurnberg: {}'.format(bfs_path[1] if bfs_path[0]
    else 'Not found'))
    print('dfs Frankfurt-Nurnberg: {}'.format(dfs_path[1] if dfs_path[0]
    else 'Not found'))
    bfs_nopath = bfs(graph, 'Wurzburg', 'Kassel')
    print('bfs Wurzburg-Kassel: {}'.format(bfs_nopath[1] if bfs_nopath[0] else
    'Not found'))
    dfs_nopath = dfs(graph, 'Wurzburg', 'Kassel')
    print('dfs Wurzburg-Kassel: {}'.format(dfs_nopath[1] if dfs_nopath[0] else
'Not found'))
if __name__ == '__main__':
	main()

從性質來看,結果並不能表明什麼,因為DFS和BFS不能很好地處理加權圖(權重完全被忽略了)。處理加權圖更好的演算法是( Dijkstra的)最短路徑優先演算法、 Bellman-Ford演算法和A*演算法等。然而,我們仍然希望按打算的那樣遍歷圖。我們期望的演算法輸出是一個城市列表,這些城市是在搜尋從Frankfurt到Nurnberg的路徑時訪問過的。

bfs Frankfurt-Nurnberg: ['Frankfurt', 'Mannheim', 'Wurzburg', 'Kassel', 'Karlsruhe',
'Erfurt', 'Nurnberg']
dfs Frankfurt-Nurnberg: ['Frankfurt', 'Mannheim', 'Karlsruhe', 'Augsburg', 'Munchen',
'Wurzburg', 'Erfurt', 'Nurnberg']
bfs Wurzburg-Kassel: Not found
dfs Wurzburg-Kassel: Not found

結果看起來沒問題。 BFS按廣度進行遍歷, DFS則按深度進行遍歷,兩個演算法都沒返回任何非期望的結果。這樣不錯,但我們的程式碼仍然有一個問題,那就是冗餘。兩個演算法之間僅有一處不同,但其餘程式碼都寫了兩遍。

是的!這個問題可以通過模板設計模式( Template design pattern)來解決。這個模式關注的是消除程式碼冗餘,其思想是我們應該無需改變演算法結構就能重新定義一個演算法的某些部分。

def traverse(graph, start, end, action):
    path = []
    visited = [start]
    while visited:
        current = visited.pop(0)
        if current not in path:
        	path.append(current)
		if current == end:
			return (True, path)
            # 兩個頂點之間沒有連線,則跳過
            if current not in graph:
                continue
		visited = action(visited, graph[current])
	return (False, path)
def extend_bfs_path(visited, current):
	return visited + current
def extend_dfs_path(visited, current):
	return current + visited

不再有bfs()和dfs()兩個函式,我們將程式碼重構為使用單個traverse()函式。 traverse()函式實際上是一個模板函式。它接受一個引數action,該引數是一個“知道”如何延伸路徑的函式。根據要使用的演算法,我們可以傳遞extend_bfs_path()或extend_dfs_path()作為目標動作。

你也許會爭論說,通過在traverse()內部新增一個條件來檢測應該使用哪個遍歷演算法,也能達到相同的結果。下面的程式碼展示了這個思路。

BFS = 1
DFS = 2
def traverse(graph, start, end, algorithm):
    path = []
    visited = [start]
    while visited:
        current = visited.pop(0)
        if current not in path:
            path.append(current)
            if current == end:
            	return (True, path)
            # 頂點不相連,則跳過
            if current not in graph:
            	continue
        if algorithm == BFS:
        	visited = extend_bfs_path(visited, graph[current])
        elif algorithm == DFS:
        	visited = extend_dfs_path(visited, graph[current])
        else:
        	raise ValueError("No such algorithm")
    return (False, path)

這個方案不足,如下:

  • [ ] 它使得traverse()難以維護。如果新增第三種方式來延伸路徑,就需要擴充套件traverse()的程式碼,再新增一個條件來檢測是否使用新的路徑延伸動作。更好的方案是traverse()能發揮作用卻好像根本不知道應該執行哪個action,因為這樣在traverse()中不要求什麼特殊邏輯。
  • [ ] 它僅對只有一行區別的演算法有效。如果存在更多區別,那麼與讓本應歸屬action的具體細節汙染traverse()函式相比,建立一個新函式會好得多。
  • [ ] 它使得traverse()更慢。這是因為每次traverse()執行時,都需要顯式地檢測應該執行哪個遍歷函式。

執行traverse()與執行dfs()或bfs()沒什麼大的不同。下面是一個示例。

bfs_path = traverse(graph, 'Frankfurt', 'Nurnberg', extend_bfs_path)
dfs_path = traverse(graph, 'Frankfurt', 'Nurnberg', extend_dfs_path)
print('bfs Frankfurt-Nurnberg: {}'.format(bfs_path[1] if bfs_path[0] else 'Not
found'))
print('dfs Frankfurt-Nurnberg: {}'.format(dfs_path[1] if dfs_path[0] else 'Not
found'))

輸出如下:

bfs Frankfurt-Nurnberg: ['Frankfurt', 'Mannheim', 'Wurzburg', 'Kassel', 'Karlsruhe',
'Erfurt', 'Nurnberg']
dfs Frankfurt-Nurnberg: ['Frankfurt', 'Mannheim', 'Karlsruhe', 'Augsburg', 'Munchen',
'Wurzburg', 'Erfurt', 'Nurnberg']
bfs Wurzburg-Kassel: Not found
dfs Wurzburg-Kassel: Not found

1. 現實生活的例子

工人的日程,特別是對於同一個公司的工人而言,非常接近於模板設計模式。所有工人都遵從或多或少相同的例行流程,但例行流程的某些特定部分區別又很大。僅在繼承對實現有益時,我們才使用它。如果沒有實際益處,則可以忽略它,並使用命令和輸入慣例。

2. 軟體的例子

Python在cmd模組中使用了模板模式,該模組用於構建面向行的命令直譯器。具體而言cmd.Cmd.cmdloop()實現了一個演算法,持續地讀取輸入命令並將命令分發到動作方法。每次迴圈之前、之後做的事情以及命令解析部分始終是相同的。這也稱為一個演算法的不變部分。變化的是實際的動作方法(易變的部分)。

Python的asyncore模組也使用了模板模式,該模組用於實現非同步套接字服務客戶端/伺服器。其中諸如asyncore.dispatcher.handle_connect_event和asyncore.dispatcher.handle_write_event()之類的方法僅包含通用程式碼。要執行特定於套接字的程式碼,這兩個方法會執行handle_connect()方法。注意,執行的是一個特定於套接字的handle_connect(),
不是asyncore.dispatcher.handle_connect()。後者僅包含一條警告。可以使用inspect模組來檢視,如下所示。

>>> python3
import inspect
import asyncore
inspect.getsource(asyncore.dispatcher.handle_connect)
" def handle_connect(self):\n self.log_info('unhandled connect
event', 'warning')\n

3. 應用案例

模板設計模式旨在消除程式碼重複。如果我們發現結構相近的(多個)演算法中有重複程式碼,則可以把演算法的不變(通用)部分留在一個模板方法/函式中,把易變(不同)的部分移到動作/鉤子方法/函式中。

頁碼標註是一個不錯的模板模式應用案例。一個頁碼標註演算法可以分為一個抽象(不變的)部分和一個具體(易變的)部分。不變的部分關注的是最大行號/頁號這部分內容。易變的部分則包含用於顯示某個已分頁特定頁面的頁首和頁尾的功能。

所有應用框架都利用了某種形式的模板模式。在使用框架來建立圖形化應用時,通常是繼承自一個類,並實現自定義行為。然而,在執行自定義行為之前,通常會呼叫一個模板方法,該方法實現了應用中一定相同的部分,比如繪製螢幕、處理事件迴圈、調整視窗大小並居中,等等。

4. 實現

我們將實現一個橫幅生成器,想法很簡單,將一段文字傳送給一個函式,該函式要生成一個包含該文字的橫幅。橫幅有多種風格,比如點或虛線圍繞文字。橫幅生成器有一個預設風格,但應該能夠使用我們自己提供的風格。

函式generate_banner()是我們的模板函式。它接受一個輸入引數( msg,希望橫幅包含的文字)和一個可選引數( style,希望使用的風格)。預設風格是dots_style,我們馬上就能看到。 generate_banner()以一個簡單的頭部和尾部來包裝帶樣式的文字。實際上,這個頭部和尾部可以複雜得多,但在這裡呼叫可以生成頭部和尾部的函式來替代僅僅輸出簡單字串也無不可。

def generate_banner(msg, style=dots_style):
    print('-- start of banner --')
    print(style(msg))
    print('-- end of banner --\n\n')

預設的dots_style()簡單地將msg首字母大寫,並在其之前和之後輸出10個點。

def dots_style(msg):
    msg = msg.capitalize()
    msg = '.' * 10 + msg + '.' * 10
    return msg

該生成器支援的另一個風格是admire_style()。該風格以大寫形式展示文字,並在檔案的每個字元之間放入一個感嘆號。

def admire_style(msg):
    msg = msg.upper()
    return '!'.join(msg)

接下來這個風格是我目前最喜歡的。 cow_style()風格使用cowpy模組生成隨機ASCII碼藝術字符,誇張地表現文字。如果你的系統中尚未安裝cowpy,可以使用下面的命令來安裝。

>>> pip3 install cowpy

cow_style()風格會執行cowpy的milk_random_cow()方法,該方法在每次cow_style()執行時用於生成一個隨機的ASCII碼藝術字符。

def cow_style(msg):
    msg = cow.milk_random_cow(msg)
    return msg

main()函式向橫幅傳送文字“happy coding”,並使用所有可用風格將橫幅輸出到標準輸出。

def main():
    msg = 'happy coding'
    [generate_banner(msg, style) for style in (dots_style, admire_style, cow_style)]

完整程式碼:

from cowpy import cow
def dots_style(msg):
    msg = msg.capitalize()
    msg = '.' * 10 + msg + '.' * 10
	return msg
def admire_style(msg):
    msg = msg.upper()
    return '!'.join(msg)
def cow_style(msg):
    msg = cow.milk_random_cow(msg)
    return msg
def generate_banner(msg, style=dots_style):
    print('-- start of banner --')
    print(style(msg))
    print('-- end of banner --\n\n')
def main():
    msg = 'happy coding'
    [generate_banner(msg, style) for style in (dots_style, admire_style, cow_style)]
if __name__ == '__main__':
	main()

輸出如下。由於cowpy的隨機性,你的cow_style()輸出也許會有所不同。

-- start of banner --
..........Happy coding..........
-- end of banner --
-- start of banner --
H!A!P!P!Y! !C!O!D!I!N!G
-- end of banner --
-- start of banner --
______________
< Happy coding >
--------------
\
 \ \_\_ _/_/
  \   \__/
      (xx)\_______
      (__)\        )\/\
       U    ||----w |
          ||     ||
-- end of banner--

5. 小結

在實現結構相近的演算法時,可以使用模板模式來消除冗餘程式碼。具體實現方式是使用動作/鉤子方法/函式來完成程式碼重複的消除,它們是Python中的一等公民。