本文爲英文書籍 Clean Code in Python Chapter 5 Using Decorators to Improve Our Code 學習筆記,建議直接看原書html
雖然通常見到裝飾器裝飾的是方法和函數,但實際容許裝飾任何類型的對象,所以咱們將探索應用於函數、方法、生成器和類的裝飾器。python
還要注意,不要將裝飾器與裝飾器設計模式(Decorator Pattern)混爲一談。設計模式
函數多是能夠被裝飾的Python對象中最簡單的表示形式。咱們能夠在函數上使用裝飾器來達成各類邏輯——能夠驗證參數、檢查前提條件、徹底改變行爲、修改簽名、緩存結果(建立原始函數的存儲版本)等等。緩存
做爲示例,咱們將建立實現重試機制的基本裝飾器,控制特定的域級異常(domain-level exception)並重試必定次數:bash
# decorator_function_1.py
import logging
from functools import wraps
logger = logging.getLogger(__name__)
class ControlledException(Exception):
"""A generic exception on the program's domain."""
pass
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
複製代碼
能夠暫時忽略@wraps,以後再介紹
retry裝飾器使用例子:app
@retry
def run_operation(task):
"""Run a particular task, simulating some failures on its execution."""
return task.run()
複製代碼
由於裝飾器只是提供的一種語法糖,實際上等於run_operation = retry(run_operation)
比較經常使用的超時重試,即可以這樣實現。dom
咱們用一個例子詳細闡述下接受參數的處理過程。 假設你想寫一個裝飾器,給函數添加日誌功能,同時容許用戶指定日誌的級別和其餘的選項。 下面是這個裝飾器的定義和使用示例:ide
from functools import wraps
import logging
def logged(level, name=None, message=None):
""" Add logging to a function. level is the logging level, name is the logger name, and message is the log message. If name and message aren't specified, they default to the function's module and name. """
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate
# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
複製代碼
初看起來,這種實現看上去很複雜,可是核心思想很簡單。 最外層的函數 logged()
接受參數並將它們做用在內部的裝飾器函數上面。 內層的函數 decorate()
接受一個函數做爲參數,而後在函數上面放置一個包裝器。 這裏的關鍵點是包裝器是可使用傳遞給 logged()
的參數的。函數
定義一個接受參數的包裝器看上去比較複雜主要是由於底層的調用序列。特別的,若是你有下面這個代碼:學習
@decorator(x, y, z)
def func(a, b):
pass
複製代碼
裝飾器處理過程跟下面的調用是等效的;
def func(a, b):
pass
func = decorator(x, y, z)(func)
decorator(x, y, z) 的返回結果必須是一個可調用對象,它接受一個函數做爲參數幷包裝它
複製代碼
有些人認爲,裝飾類是比較複雜的事情,並且這樣的方案可能危及可讀性。由於咱們在類中聲明一些屬性和方法,可是裝飾器可能會改變它們的行爲,呈現出徹底不一樣的類。
在這種技術被嚴重濫用的狀況下,這種評價是正確的。客觀地說,這與裝飾函數沒有什麼不一樣;畢竟,類只是Python生態系統中的另外一種類型的對象,就像函數同樣。咱們將在標題爲「裝飾器和關注點分離」的章節中一塊兒回顧這個問題的利弊,可是如今,咱們將探討類的裝飾器的好處:
回顧監視平臺的事件系統,咱們如今須要轉換每一個事件的數據並將其發送到外部系統。 可是,在選擇如何發送數據時,每種類型的事件可能都有本身的特殊性。
特別是,登陸的事件可能包含敏感信息,如登陸信息須要隱藏, 時間戳等其餘字段也可能須要特定的格式顯示。
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()
複製代碼
在這裏,咱們聲明一個類,該類將直接映射到登陸事件,包含其邏輯——隱藏密碼字段,並根據須要格式化時間戳。
雖然這種方法可行,並且看起來是個不錯的選擇,可是隨着時間的推移,想要擴展咱們的系統,就會發現一些問題:
另外一種解決方案是,給定一組過濾器(轉換函數)和一個事件實例,可以動態構造對象,該對象可以經過濾器對其字段序列化。而後,咱們只須要定義轉換每種類型的字段的函數,而且經過組合這些函數中的許多函數來建立序列化程序。
一旦有了這個對象,咱們就能夠裝飾類,以便添加serialize()方法,該方法將只調用這些Serialization對象自己:
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_field
class 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
複製代碼
待續。。。