Python很早就引入了裝飾器——在PEP-318中,做爲一種簡化函數和方法定義方式的機制,這些函數和方法在初始定義以後必須進行修改。python
這樣作的最初動機之一是,使用classmethod和staticmethod等函數來轉換方法的原始定義,可是它們須要額外的一行代碼來修改函數的初始定義。編程
通常來講,每次必須對函數應用轉換時,咱們必須使用modifier函數調用它,而後將它從新分配到函數初始定義時的名稱中。設計模式
例如,假設有一個叫做original的函數,在它上面有一個改變original行爲的函數(叫做modifier),那麼咱們必須這樣寫:緩存
def original(...): ... original = modifier(original)
請注意咱們是如何更改函數並將其從新分配到相同的名稱中去的。這是使人困惑的,很容易出錯(假設有人忘記從新分配函數,或者從新分配了函數,但不在函數定義以後的行中,而是在更遠的地方),並且很麻煩。出於這個緣由,Python語言增長了一些語法支持。ruby
前面的示例能夠改寫爲以下樣式:閉包
@modifierdef original(...): ...
這意味着裝飾器只是語法糖,用於調用裝飾器以後的內容做爲裝飾器自己的第一個參數,結果將是裝飾器返回的內容。架構
爲了與Python的術語一致,在咱們的示例中modifier稱爲裝飾器,original是裝飾函數,一般也被稱爲包裝對象。app
雖然該功能最初被認爲是用於方法和函數的,但實際的語法容許它修飾任何類型的對象,所以咱們將研究應用於函數、方法、生成器和類的裝飾器。dom
最後一點須要注意的是,雖然裝飾器的名稱是正確的(畢竟,裝飾器其實是在對包裝函數進行更改、擴展或處理),但不要將它與裝飾器設計模式混淆。ide
函數多是對能夠裝飾的Python對象的最簡單的表示形式。咱們能夠在函數上使用裝飾器來應用各類邏輯——咱們能夠驗證參數、檢查前置條件、徹底改變行爲、修改其簽名、緩存結果(建立原始函數的內存版本)等。
例如,咱們將建立一個實現retry機制的基本裝飾器,控制一個特定的域級異常並重試必定的次數:
# decorator_function_1.pyclass ControlledException(Exception): """A generic exception on the program's domain."""def retry(operation): @wraps(operation) def wrapped(*args, **kwargs): last_raised = None RETRIES_LIMIT = 3 for _ in range(RETRIES_LIMIT): try: return operation(*args, **kwargs) except ControlledException as e: logger.info("retrying %s", operation.__qualname__) last_raised = e raise last_raised return wrapped
如今能夠忽略@wrap的使用,由於它將在另外一節中討論。在for循環中使用「_」,意味着這個數字被分配給一個咱們目前不感興趣的變量,由於它不在for循環中使用(在Python中,將被忽略的值命名爲「_」是一個常見的習慣用法)。
retry裝飾器不接收任何參數,因此它能夠很容易地應用於任何函數,以下所示:
@retrydef run_operation(task): """Run a particular task, simulating some failures on its execution.""" return task.run()
正如一開始所解釋的,在run_operation之上@retry的定義只是Python提供的語法糖,用於實際執行run_operation = retry(run_operation)。
在這個有限的示例中,咱們能夠看到如何用裝飾器建立一個通用的retry操做,在某些肯定的條件下(在本示例中,表示爲可能與超時相關的異常),該操做將容許屢次調用裝飾後的代碼。
類也能夠被裝飾(PEP-3129),其裝飾方法與語法函數的裝飾方法相同。惟一的區別是,在爲裝飾器編寫代碼時,咱們必須考慮到所接收的是一個類,而不是一個函數。
一些實踐者可能會認爲裝飾類是至關複雜的事情,這樣的場景可能會損害可讀性,由於咱們將在類中聲明一些屬性和方法,可是在幕後,裝飾器可能會應用一些變化,從而呈現一個徹底不一樣的類。
這種評定是正確的,但只有在裝飾類技術被嚴重濫用的狀況下成立。客觀上,這與裝飾功能沒有什麼不一樣;畢竟,類和函數同樣,都只是Python生態系統中的一種類型的對象而已。在5.4節中,咱們將再次審視這個問題的優缺點,可是這裏只探索裝飾器的優勢,尤爲是適用於類的裝飾器的優勢。
(1)重用代碼和DRY原則的全部好處。類裝飾器的一個有效狀況是,強制多個類符合特定的接口或標準(經過只在將應用於多個類的裝飾器中進行一次檢查)。
(2)能夠建立更小或更簡單的類——這些類稍後將由裝飾器進行加強。
(3)若是使用裝飾器,那麼須要應用到特定類上的轉換邏輯將更容易維護,而不會使用更復雜的(一般是不鼓勵使用的)方法,如元類。
在裝飾器的全部可能應用程序中,咱們將探索一個簡單的示例,以瞭解裝飾器能夠用於哪些方面。記住,這不是類裝飾器的惟一應用程序類型,並且給出的代碼還能夠有許多其餘解決方案。全部這些解決方案都有優缺點,之因此選擇裝飾器,是爲了說明它們的用處。
回顧用於監視平臺的事件系統,如今須要轉換每一個事件的數據並將其發送到外部系統。然而,在選擇如何發送數據時,每種類型的事件可能都有本身的特殊性。
特別是,登陸事件可能包含敏感信息,例如咱們但願隱藏的憑據。時間戳等其餘領域的字段可能也須要一些轉換,由於咱們但願以特定的格式顯示它們。符合這些要求的第一次嘗試很簡單,就像有一個映射到每一個特定事件的類,並知道如何序列化它那樣:
class LoginEventSerializer: def __init__(self, event): self.event = event def serialize(self) -> dict: return { "username": self.event.username, "password": "**redacted**", "ip": self.event.ip, "timestamp": self.event.timestamp.strftime("%Y-%m-%d %H:%M"), }class LoginEvent: SERIALIZER = LoginEventSerializer def __init__(self, username, password, ip, timestamp): self.username = username self.password = password self.ip = ip self.timestamp = timestamp def serialize(self) -> dict: return self.SERIALIZER(self).serialize()
在這裏,咱們聲明一個類。該類將直接映射到登陸事件,其中包含它的一些邏輯——隱藏密碼字段,並根據須要格式化時間戳。
雖然這是可行的,可能開始看起來是一個不錯的選擇,但隨着時間的推移,若要擴展系統,就會發現一些問題。
(1)類太多。隨着事件數量的增多,序列化類的數量將以相同的量級增加,由於它們是一一映射的。
(2)解決方案不夠靈活。若是咱們須要重用部分組件(例如,須要把密碼藏在也有相似需求的另外一個類型的事件中),就不得不將其提取到一個函數,但也要從多個類中調用它,這意味着咱們沒有重用那麼多代碼。
(3)樣板文件。serialize()方法必須出如今全部事件類中,同時調用相同的代碼。儘管咱們能夠將其提取到另外一個類中(建立mixin),但這彷佛沒有很好地使用繼承。
另外一種解決方案是可以動態構造一個對象:給定一組過濾器(轉換函數)和一個事件實例,該對象可以經過將過濾器應用於其字段的方式序列化它。而後,咱們只須要定義轉換每種字段類型的函數,並經過組合這些函數建立序列化器。
一旦有了這個對象,咱們就能夠裝飾類以添加serialize()方法。該方法只會調用這些序列化對象自己:
def hide_field(field) -> str: return "**redacted**"def format_time(field_timestamp: datetime) -> str: return field_timestamp.strftime("%Y-%m-%d %H:%M")def show_original(event_field): return event_fieldclass EventSerializer: def __init__(self, serialization_fields: dict) -> None: self.serialization_fields = serialization_fields def serialize(self, event) -> dict: return { field: transformation(getattr(event, field)) for field, transformation in self.serialization_fields.items() }class Serialization: def __init__(self, **transformations): self.serializer = EventSerializer(transformations) def __call__(self, event_class): def serialize_method(event_instance): return self.serializer.serialize(event_instance) event_class.serialize = serialize_method return event_class @Serialization( username=show_original, password=hide_field, ip=show_original, timestamp=format_time, )class LoginEvent: def __init__(self, username, password, ip, timestamp): self.username = username self.password = password self.ip = ip self.timestamp = timestamp
注意,裝飾器讓你更容易知道如何處理每一個字段,而沒必要查看另外一個類的代碼。僅經過讀取傳遞給類裝飾器的參數,咱們就知道用戶名和IP地址將保持不變,密碼將被隱藏,時間戳將被格式化。
如今,類的代碼不須要定義serialize()方法,也不須要從實現它的mixin類進行擴展,由於這些都將由裝飾器添加。實際上,這多是建立類裝飾器的惟一理由,由於若是不是這樣的話,序列化對象多是LoginEvent的一個類屬性,可是它經過向該類添加一個新方法來更改類,這使得建立該類裝飾器變得不可能。
咱們還可使用另外一個類裝飾器,經過定義類的屬性來實現init方法的邏輯,但這超出了本例的範圍。
經過使用Python 3.7+ 中的這個類裝飾器(PEP-557),能夠按更簡潔的方式重寫前面的示例,而不使用init的樣板代碼,以下所示:
from dataclasses import dataclassfrom datetime import datetime@Serialization( username=show_original, password=hide_field, ip=show_original, timestamp=format_time,)@dataclassclass LoginEvent: username: str password: str ip: str timestamp: datetime
既然咱們已經知道了裝飾器的@語法的實際含義,就能夠得出這樣的結論:能夠裝飾的不只是函數、方法或類;實際上,任何能夠定義的東西(如生成器、協同程序甚至是裝飾過的對象)均可以裝飾,這意味着裝飾器能夠堆疊起來。
前面的示例展現瞭如何連接裝飾器。咱們先定義類,而後將@dataclass應用於該類——它將該類轉換爲數據類,充當這些屬性的容器。以後,經過@Serialization把邏輯應用到該類上,從而生成一個新類,其中添加了新的serialize()方法。
裝飾器另外一個好的用法是用於應該用做協同程序的生成器。咱們將在第7章中探討生成器和協同程序的細節,其主要思想是,在向新建立的生成器發送任何數據以前,必須經過調用next()將後者推動到下一個yield語句。這是每一個用戶都必須記住的手動過程,所以很容易出錯。咱們能夠輕鬆建立一個裝飾器,使其接收生成器做爲參數,調用next(),而後返回生成器。
至此,咱們已經將裝飾器看做Python中的一個強大工具。若是咱們能夠將參數傳遞給裝飾器,使其邏輯更加抽象,那麼其功能可能會更增強大。
有幾種實現裝飾器的方法能夠接收參數,可是接下來咱們只討論最多見的方法。第一種方法是將裝飾器建立爲帶有新的間接層的嵌套函數,使裝飾器中的全部內容深刻一層。第二種方法是爲裝飾器使用一個類。
一般,第二種方法更傾向於可讀性,由於從對象的角度考慮,其要比3個或3個以上使用閉包的嵌套函數更容易。可是,爲了完整起見,咱們將對這兩種方法進行探討,以便你能夠選擇使用最適合當前問題的方法。
粗略地說,裝飾器的基本思想是建立一個返回函數的函數(一般稱爲高階函數)。在裝飾器主體中定義的內部函數將是實際被調用的函數。
如今,若是但願將參數傳遞給它,就須要另外一間接層。第一個函數將接收參數,在該函數中,咱們將定義一個新函數(它將是裝飾器),而這個新函數又將定義另外一個新函數,即裝飾過程返回的函數。這意味着咱們將至少有3層嵌套函數。
若是你到目前爲止還不明白上述內容的含義,也不用擔憂,待查看下面給出的示例以後,就會明白了。
第一個示例是,裝飾器在一些函數上實現重試功能。這是個好主意,只是有個問題:實現不容許指定重試次數,只容許在裝飾器中指定一個固定的次數。
如今,咱們但願可以指出每一個示例有多少次重試,也許甚至能夠爲這個參數添加一個默認值。爲了實現這個功能,咱們須要用到另外一層嵌套函數——先用於參數,而後用於裝飾器自己。
這是由於以下代碼:
@retry(arg1, arg2,... )
必須返回裝飾器,由於@語法將把計算結果應用到要裝飾的對象上。從語義上講,它能夠翻譯成以下內容:
<original_function> = retry(arg1, arg2, ....)(<original_function>)
除了所需的重試次數,咱們還能夠指明但願控制的異常類型。支持新需求的新版本代碼多是這樣的:
RETRIES_LIMIT = 3def with_retry(retries_limit=RETRIES_LIMIT, allowed_exceptions=None): allowed_exceptions = allowed_exceptions or (ControlledException,) def retry(operation): @wraps(operation) def wrapped(*args, **kwargs): last_raised = None for _ in range(retries_limit): try: return operation(*args, **kwargs) except allowed_exceptions as e: logger.info("retrying %s due to %s", operation, e) last_raised = e raise last_raised return wrapped return retry
下面是這個裝飾器如何應用於函數的一些示例,其中顯示了它接收的不一樣選項:
# decorator_parametrized_1.py@with_retry()def run_operation(task): return task.run()@with_retry(retries_limit=5)def run_with_custom_retries_limit(task): return task.run()@with_retry(allowed_exceptions=(AttributeError,))def run_with_custom_exceptions(task): return task.run()@with_retry( retries_limit=4, allowed_exceptions=(ZeroDivisionError, AttributeError) )def run_with_custom_parameters(task): return task.run()
前面的示例須要用到3層嵌套函數。首先,這將是一個用於接收咱們想要使用的裝飾器的參數。在這個函數中,其他的函數是使用這些參數和裝飾器邏輯的閉包。
更簡潔的實現方法是用一個類定義裝飾器。在這種狀況下,咱們能夠在__init__方法中傳遞參數,而後在名爲__call__的魔法方法上實現裝飾器的邏輯。
裝飾器的代碼以下所示:
class WithRetry: def __init__(self, retries_limit=RETRIES_LIMIT, allowed_exceptions=None): self.retries_limit = retries_limit self.allowed_exceptions = allowed_exceptions or(ControlledException,) def __call__(self, operation): @wraps(operation) def wrapped(*args, **kwargs): last_raised = None for _ in range(self.retries_limit): try: return operation(*args, **kwargs) except self.allowed_exceptions as e: logger.info("retrying %s due to %s", operation, e) last_raised = e raise last_raised return wrapped
這個裝飾器能夠像以前的同樣應用,就像這樣:
@WithRetry(retries_limit=5)def run_with_custom_retries_limit(task): return task.run()
注意Python語法在這裏是如何起做用的,這一點很重要。首先,咱們建立對象,這樣在應用@操做以前,對象已經建立好了,而且其參數傳遞給它了,用這些參數初始化這個對象,如init方法中定義的那樣。在此以後,咱們將調用@操做,這樣該對象將包裝名爲run_with_custom_reries_limit的函數,而這意味着它將被傳遞給call這個魔法方法。
在call這個魔法方法中,咱們定義了裝飾器的邏輯,就像一般所作的那樣——包裝了原始函數,返回一個新的函數,其中包含所要的邏輯。
本節介紹一些充分利用裝飾器的常見模式。在有些常見的場景中使用裝飾器是個很是好的選擇。
可用於應用程序的裝飾器數不勝數,下面僅列舉幾個最多見或相關的。
(1)轉換參數。更改函數的簽名以公開更好的API,同時封裝關於如何處理和轉換參數的詳細信息。
(2)跟蹤代碼。記錄函數及其參數的執行狀況。
(3)驗證參數。
(4)實現重試操做。
(5)經過把一些(重複的)邏輯移到裝飾器中來簡化類。
接下來詳細討論前兩個應用程序。
前文提到,裝飾器能夠用來驗證參數(甚至在DbC的概念下強制一些前置條件或後置條件),所以你可能已經瞭解到,這是一些處理或者操控參數時使用裝飾器的經常使用方法。
特別是,在某些狀況下,咱們會發現本身反覆建立相似的對象,或者應用相似的轉換,而咱們但願將這些轉換抽象掉。大多數時候,咱們能夠經過簡單地用裝飾器實現這一點。
在本節中討論跟蹤時,咱們將提到一些更通用的內容,這些內容與處理所要監控的函數的執行有關,具體是指:
(1)實際跟蹤函數的執行(例如,經過記錄函數執行的行);
(2)監控函數的一些指標(如CPU使用量或內存佔用);
(3)測量函數的運行時間;
(4)函數被調用時的日誌,以及傳遞給它的參數。
咱們將在5.2節剖析一個簡單的裝飾器示例,該示例記錄了函數的執行狀況,包括函數名和運行時間。
本文摘自《編寫整潔的Python代碼》
本書介紹Python軟件工程的主要實踐和原則,旨在幫助讀者編寫更易於維護和更整潔的代碼。全書共10章:第1章介紹Python語言的基礎知識和搭建Python開發環境所需的主要工具;第2章描述Python風格代碼,介紹Python中的第一個習慣用法;第3章總結好代碼的通常特徵,回顧軟件工程中的通常原則;第4章介紹一套面向對象軟件設計的原則,即SOLID原則;第5章介紹裝飾器,它是Python的**特性之一;第6章探討描述符,介紹如何經過描述符從對象中獲取更多的信息;第7章和第8章介紹生成器以及單元測試和重構的相關內容;第9章回顧Python中最多見的設計模式;第10章再次強調代碼整潔是實現良好架構的基礎。
本書適合全部Python編程愛好者、對程序設計感興趣的人,以及其餘想學習更多Python知識的軟件工程的從業人員。