Python 設計模式:用於時尚時尚的代碼
已發表: 2022-03-11再說一遍:Python 是一種具有動態類型和動態綁定的高級編程語言。 我會將其描述為一種功能強大的高級動態語言。 許多開發人員都愛上了 Python,因為它的語法清晰、結構良好的模塊和包,以及它巨大的靈活性和現代特性的範圍。
在 Python 中,沒有什麼要求您編寫類並從中實例化對象。 如果您的項目中不需要復雜的結構,您可以只編寫函數。 更好的是,您可以編寫一個平面腳本來執行一些簡單而快速的任務,而無需構建代碼。
同時 Python 是 100% 面向對象的語言。 怎麼樣? 嗯,簡單地說, Python 中的一切都是對象。 函數是對象,第一類對象(無論這意味著什麼)。 函數是對像這一事實很重要,所以請記住這一點。
因此,您可以在 Python 中編寫簡單的腳本,或者直接打開 Python 終端並在此處執行語句(這非常有用!)。 但同時,您可以創建複雜的框架、應用程序、庫等。 你可以在 Python 中做很多事情。 當然有一些限制,但這不是本文的主題。
然而,由於 Python 如此強大和靈活,我們在使用它進行編程時需要一些規則(或模式)。 那麼,讓我們看看什麼是模式,以及它們與 Python 的關係。 我們還將繼續實現一些基本的 Python 設計模式。
為什麼 Python 對模式有好處?
任何編程語言都適用於模式。 事實上,模式應該在任何給定編程語言的上下文中考慮。 模式、語言語法和性質都對我們的編程施加了限制。 來自語言語法和語言性質(動態、功能、面向對像等)的限制可能不同,它們存在的原因也可能不同。 來自模式的限制是有原因的,它們是有目的的。 這是模式的基本目標; 告訴我們如何做某事以及如何不做。 稍後我們將討論模式,尤其是 Python 設計模式。
Python 的哲學建立在經過深思熟慮的最佳實踐的理念之上。 Python 是一種動態語言(我已經說過了嗎?),因此,它已經實現了或使其易於實現,只需幾行代碼即可實現許多流行的設計模式。 一些設計模式是內置在 Python 中的,所以我們甚至在不知道的情況下使用它們。 由於語言的性質,不需要其他模式。
例如, Factory是一種結構化 Python 設計模式,旨在創建新對象,對用戶隱藏實例化邏輯。 但是在 Python 中創建對像是動態設計的,因此不需要像 Factory 這樣的添加。 當然,如果你願意,你可以自由地實現它。 可能在某些情況下它會非常有用,但它們是一個例外,而不是常態。
Python 的哲學有什麼好的? 讓我們從這個開始(在 Python 終端中探索它):
> >> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
這些可能不是傳統意義上的模式,但這些規則以最優雅和最有用的方式定義了“Pythonic”編程方法。
我們還有 PEP-8 代碼指南來幫助構建我們的代碼。 當然,這對我來說是必須的,當然也有一些適當的例外。 順便說一句,PEP-8 本身鼓勵這些例外:
但最重要的是:知道何時不一致——有時風格指南並不適用。 如有疑問,請使用您的最佳判斷。 查看其他示例並決定什麼看起來最好。 不要猶豫,問!
將 PEP-8 與 The Zen of Python(也是 PEP - PEP-20)結合起來,您將擁有創建可讀和可維護代碼的完美基礎。 添加設計模式,您就可以創建具有一致性和可演化性的各種軟件系統。
Python 設計模式
什麼是設計模式?
一切都始於四人幫 (GOF)。 如果您不熟悉 GOF,請進行快速在線搜索。
設計模式是解決眾所周知的問題的常用方法。 GOF 定義的設計模式的基礎有兩個主要原則:
- 編程到接口而不是實現。
- 優先考慮對象組合而不是繼承。
讓我們從 Python 程序員的角度來仔細看看這兩個原則。
編程到接口而不是實現
想想鴨子打字。 在 Python 中,我們不喜歡根據這些接口定義接口和程序類,是嗎? 但是,聽我說! 這並不意味著我們不考慮接口,實際上使用 Duck Typing 我們一直都在這樣做。
讓我們對臭名昭著的 Duck Typing 方法說幾句,看看它是如何適應這種範式的:程序到接口。
我們不關心對象的性質,我們不必關心對像是什麼; 我們只想知道它是否能夠做我們需要的事情(我們只對對象的接口感興趣)。
物體會嘎嘎叫嗎? 所以,讓它嘎嘎叫吧!
try: bird.quack() except AttributeError: self.lol()
我們是否為我們的鴨子定義了一個接口? 不! 我們是否針對接口而不是實現進行編程? 是的! 而且,我覺得這很好。
正如 Alex Martelli 在他關於 Python 中的設計模式的著名演講中指出的那樣, “教鴨子打字需要一段時間,但之後會為您節省大量工作!”
優先考慮對象組合而不是繼承
現在,這就是我所說的Pythonic原則! 與在另一個類中包裝一個類(或更常見的是幾個類)相比,我創建了更少的類/子類。
而不是這樣做:
class User(DbObject): pass
我們可以這樣做:
class User: _persist_methods = ['get', 'save', 'delete'] def __init__(self, persister): self._persister = persister def __getattr__(self, attribute): if attribute in self._persist_methods: return getattr(self._persister, attribute)
優勢是顯而易見的。 我們可以限制要暴露的包裝類的哪些方法。 我們可以在運行時注入持久化實例! 例如,今天它是一個關係數據庫,但明天它可以是任何我們需要的接口(同樣是那些討厭的鴨子)。
組合對 Python 來說是優雅而自然的。
行為模式
行為模式涉及對象之間的通信,對像如何交互和完成給定任務。 根據 GOF 原則,Python 中共有 11 種行為模式:責任鏈、命令、解釋器、迭代器、調解器、備忘錄、觀察者、狀態、策略、模板、訪問者。
我發現這些模式非常有用,但這並不意味著其他模式組沒有。
迭代器
迭代器內置在 Python 中。 這是該語言最強大的特徵之一。 多年前,我在某處讀到迭代器使 Python 變得很棒,我認為情況仍然如此。 對 Python 迭代器和生成器有足夠的了解,你就會知道關於這個特定的 Python 模式所需的一切。
責任鏈
這種模式為我們提供了一種使用不同方法處理請求的方法,每種方法都針對請求的特定部分。 你知道,優秀代碼的最佳原則之一是單一職責原則。
每段代碼都必須做一件事,而且只能做一件事。
這個原則深深地融入了這個設計模式。
例如,如果我們想要過濾一些內容,我們可以實現不同的過濾器,每個過濾器都執行一種精確且明確定義的過濾類型。 這些過濾器可用於過濾冒犯性詞語、廣告、不合適的視頻內容等。
class ContentFilter(object): def __init__(self, filters=None): self._filters = list() if filters is not None: self._filters += filters def filter(self, content): for filter in self._filters: content = filter(content) return content filter = ContentFilter([ offensive_filter, ads_filter, porno_video_filter]) filtered_content = filter.filter(content)
命令
這是我作為程序員實現的第一個 Python 設計模式之一。 這提醒了我:模式不是發明的,而是被發現的。 它們存在,我們只需要找到並使用它們。 我在多年前實施的一個驚人項目中發現了這個:一個特殊用途的 WYSIWYM XML 編輯器。 在代碼中大量使用這種模式之後,我在一些網站上閱讀了更多關於它的信息。
命令模式在某些情況下很方便,因為某種原因,我們需要首先準備將要執行的內容,然後在需要時執行它。 這樣做的好處是,以這種方式封裝動作使 Python 開發人員能夠添加與已執行動作相關的附加功能,例如撤消/重做,或保留動作歷史記錄等。
讓我們看看一個簡單且經常使用的示例是什麼樣的:
class RenameFileCommand(object): def __init__(self, from_name, to_name): self._from = from_name self._to = to_name def execute(self): os.rename(self._from, self._to) def undo(self): os.rename(self._to, self._from) class History(object): def __init__(self): self._commands = list() def execute(self, command): self._commands.append(command) command.execute() def undo(self): self._commands.pop().undo() history = History() history.execute(RenameFileCommand('docs/cv.doc', 'docs/cv-en.doc')) history.execute(RenameFileCommand('docs/cv1.doc', 'docs/cv-bg.doc')) history.undo() history.undo()
創作模式
讓我們首先指出創建模式在 Python 中並不常用。 為什麼? 因為語言的動態特性。
比我更聰明的人曾經說過,Factory 是內置在 Python 中的。 這意味著語言本身為我們提供了以足夠優雅的方式創建對象所需的所有靈活性; 我們很少需要在上面實現任何東西,比如 Singleton 或 Factory。
在一個 Python 設計模式教程中,我找到了對創建設計模式的描述,其中指出這些設計“模式提供了一種在隱藏創建邏輯的同時創建對象的方法,而不是直接使用new運算符實例化對象。”
這幾乎總結了問題:我們在 Python 中沒有新的運算符!
儘管如此,讓我們看看我們如何實現一些,如果我們覺得我們可能會通過使用這些模式獲得優勢。
辛格爾頓
當我們想要保證在運行時只存在給定類的一個實例時,使用單例模式。 我們真的需要 Python 中的這種模式嗎? 根據我的經驗,故意創建一個實例然後使用它而不是實現單例模式更容易。
但是如果你想實現它,這裡有一些好消息:在 Python 中,我們可以改變實例化過程(以及幾乎任何其他東西)。 還記得我之前提到的__new__()
方法嗎? 開始了:

class Logger(object): def __new__(cls, *args, **kwargs): if not hasattr(cls, '_logger'): cls._logger = super(Logger, cls ).__new__(cls, *args, **kwargs) return cls._logger
在這個例子中, Logger是一個 Singleton。
這些是在 Python 中使用 Singleton 的替代方法:
- 使用一個模塊。
- 在應用程序的頂層某處創建一個實例,可能在配置文件中。
- 將實例傳遞給需要它的每個對象。 這是一種依賴注入,它是一種強大且易於掌握的機制。
依賴注入
我不打算討論依賴注入是否是一種設計模式,但我會說它是一種非常好的實現鬆散耦合的機制,它有助於使我們的應用程序可維護和可擴展。 將它與 Duck Typing 結合使用,原力將與您同在。 總是。
我在這篇文章的創建模式部分列出了它,因為它處理了何時(或者更好:在哪裡)創建對象的問題。 它是在外面創建的。 最好說對像根本不是在我們使用它們的地方創建的,因此依賴關係不是在使用它的地方創建的。 消費者代碼接收外部創建的對象並使用它。 如需進一步參考,請閱讀此 Stackoverflow 問題的最受好評的答案。
這是對依賴注入的一個很好的解釋,讓我們對這種特殊技術的潛力有了一個很好的了解。 基本上,答案通過以下示例解釋了問題:不要自己從冰箱裡拿東西喝,而是說明需要。 告訴你的父母你需要在午餐時喝點東西。
Python 為我們提供了輕鬆實現它所需的一切。 想想它在 Java 和 C# 等其他語言中的可能實現,你會很快意識到 Python 的美妙之處。
讓我們考慮一個簡單的依賴注入示例:
class Command: def __init__(self, authenticate=None, authorize=None): self.authenticate = authenticate or self._not_authenticated self.authorize = authorize or self._not_autorized def execute(self, user, action): self.authenticate(user) self.authorize(user, action) return action() if in_sudo_mode: command = Command(always_authenticated, always_authorized) else: command = Command(config.authenticate, config.authorize) command.execute(current_user, delete_user_action)
我們在 Command 類中註入驗證器和授權器方法。 Command 類所需要的只是成功執行它們,而不用擔心實現細節。 這樣,我們可以將 Command 類與我們決定在運行時使用的任何身份驗證和授權機制一起使用。
我們已經展示瞭如何通過構造函數注入依賴項,但我們可以通過直接設置對象屬性輕鬆注入它們,從而釋放更多潛力:
command = Command() if in_sudo_mode: command.authenticate = always_authenticated command.authorize = always_authorized else: command.authenticate = config.authenticate command.authorize = config.authorize command.execute(current_user, delete_user_action)
還有更多關於依賴注入的知識; 例如,好奇的人會搜索 IoC。
但在您這樣做之前,請閱讀另一個 Stackoverflow 答案,這是對該問題最受好評的答案。
同樣,我們剛剛演示瞭如何在 Python 中實現這種美妙的設計模式只是使用該語言的內置功能的問題。
讓我們不要忘記這一切意味著什麼:依賴注入技術允許非常靈活和簡單的單元測試。 想像一個架構,您可以在其中動態更改數據存儲。 模擬數據庫成為一項微不足道的任務,不是嗎? 有關更多信息,您可以查看 Toptal 的 Python 模擬簡介。
您可能還想研究Prototype 、 Builder和Factory設計模式。
結構模式
正面
這很可能是最著名的 Python 設計模式。
想像一下,您有一個包含大量對象的系統。 每個對像都提供一組豐富的 API 方法。 你可以用這個系統做很多事情,但是簡化界面怎麼樣? 為什麼不添加一個接口對象來公開所有 API 方法的經過深思熟慮的子集? 門面!
Python Facade 設計模式示例:
class Car(object): def __init__(self): self._tyres = [Tyre('front_left'), Tyre('front_right'), Tyre('rear_left'), Tyre('rear_right'), ] self._tank = Tank(70) def tyres_pressure(self): return [tyre.pressure for tyre in self._tyres] def fuel_level(self): return self._tank.level
沒有驚喜,沒有技巧, Car
類是Facade ,僅此而已。
適配器
如果使用Facades來簡化界面,那麼Adapters就是為了改變界面。 就像系統期待鴨子時使用牛一樣。
假設您有一種將信息記錄到給定目的地的工作方法。 您的方法期望目標具有write()
方法(例如,每個文件對像都有)。
def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message))
我會說這是一個寫得很好的依賴注入方法,它具有很好的可擴展性。 假設你想記錄到某個 UDP 套接字而不是文件,你知道如何打開這個 UDP 套接字,但唯一的問題是socket
對像沒有write()
方法。 你需要一個適配器!
import socket class SocketWriter(object): def __init__(self, ip, port): self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._ip = ip self._port = port def write(self, message): self._socket.send(message, (self._ip, self._port)) def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message)) upd_logger = SocketWriter('1.2.3.4', '9999') log('Something happened', udp_destination)
但為什麼我發現適配器如此重要? 好吧,當它與依賴注入有效結合時,它為我們提供了巨大的靈活性。 當我們只需實現一個適配器,將新接口轉換為眾所周知的接口時,為什麼要更改我們經過良好測試的代碼以支持新接口?
您還應該檢查並掌握橋接和代理設計模式,因為它們與適配器相似。 想一想在 Python 中實現它們有多麼容易,並想一想在項目中使用它們的不同方式。
裝飾者
哦,我們是多麼幸運! 裝飾器非常好,我們已經將它們集成到語言中。 我最喜歡 Python 的地方在於,使用它可以教會我們使用最佳實踐。 並不是說我們不必意識到最佳實踐(尤其是設計模式),但是對於 Python,我覺得我無論如何都在遵循最佳實踐。 就我個人而言,我發現 Python 的最佳實踐是直觀的和第二天性的,這是新手和精英開發人員都喜歡的東西。
裝飾器模式是關於引入額外的功能,特別是在不使用繼承的情況下進行。
那麼,讓我們看看我們如何在不使用 Python 內置功能的情況下裝飾方法。 這是一個簡單的例子。
def execute(user, action): self.authenticate(user) self.authorize(user, action) return action()
這裡不太好的地方在於, execute
函數的作用遠不止執行某些東西。 我們沒有嚴格遵守單一責任原則。
最好只寫以下內容:
def execute(action): return action()
我們可以在另一個地方,在裝飾器中實現任何授權和身份驗證功能,如下所示:
def execute(action, *args, **kwargs): return action() def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizeddError return decorated execute = authenticated_only(execute) execute = authorized_only(execute)
現在execute()
方法是:
- 簡單易讀
- 只做一件事(至少在查看代碼時)
- 裝飾有認證
- 裝飾有授權
我們使用 Python 的集成裝飾器語法編寫相同的代碼:
def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs ) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizedError return decorated @authorized_only @authenticated_only def execute(action, *args, **kwargs): return action()
重要的是要注意,您不僅限於作為裝飾器的功能。 裝飾器可能涉及整個類。 唯一的要求是它們必須是callables 。 但是我們對此沒有任何問題; 我們只需要定義__call__(self)
方法。
您可能還想仔細查看 Python 的 functools 模塊。 那裡有很多發現!
結論
我已經展示了使用 Python 的設計模式是多麼自然和容易,但我也展示了 Python 編程應該如何輕鬆進行。
“簡單勝於復雜”,還記得嗎? 也許您已經註意到,沒有一個設計模式被完整而正式地描述。 沒有顯示複雜的全面實施。 您需要以最適合您的風格和需求的方式“感受”並實施它們。 Python 是一門很棒的語言,它為您提供了生成靈活和可重用代碼所需的所有功能。
然而,它給你的遠不止這些。 它給了你編寫非常糟糕的代碼的“自由”。 不要這樣做! 不要重複自己 (DRY) 並且永遠不要編寫超過 80 個字符的代碼行。 並且不要忘記在適用的情況下使用設計模式; 這是向他人學習並免費從他們的豐富經驗中獲益的最佳方式之一。