Python代碼整潔之道--使用裝飾器改進代碼

本文爲英文書籍 Clean Code in Python Chapter 5 Using Decorators to Improve Our Code 學習筆記,建議直接看原書html

  • 瞭解Python中裝飾器的工做原理
  • 學習如何實現應用於函數和類的裝飾器
  • 有效地實現裝飾器,避免常見的執行錯誤
  • 分析如何用裝飾器避免代碼重複(DRY)
  • 研究裝飾器如何爲關注點分離作出貢獻
  • 優秀裝飾器實例分析
  • 回顧常見狀況、習慣用法或模式,瞭解什麼時候裝飾器是正確的選擇

雖然通常見到裝飾器裝飾的是方法和函數,但實際容許裝飾任何類型的對象,所以咱們將探索應用於函數、方法、生成器和類的裝飾器。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生態系統中的另外一種類型的對象,就像函數同樣。咱們將在標題爲「裝飾器和關注點分離」的章節中一塊兒回顧這個問題的利弊,可是如今,咱們將探討類的裝飾器的好處:

  • 代碼重用和DRY。一個恰當的例子是,類裝飾器強制多個類符合某個特定的接口或標準(經過在裝飾器中僅檢查一次,而能應用於多個類)
  • 能夠建立更小或更簡單的類,而經過裝飾器加強這些類
  • 類的轉換邏輯將更容易維護,而不是使用更復雜(一般是理所固然不被鼓勵的)的方法,好比元類

回顧監視平臺的事件系統,咱們如今須要轉換每一個事件的數據並將其發送到外部系統。 可是,在選擇如何發送數據時,每種類型的事件可能都有本身的特殊性。

特別是,登陸的事件可能包含敏感信息,如登陸信息須要隱藏, 時間戳等其餘字段也可能須要特定的格式顯示。

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()
複製代碼

在這裏,咱們聲明一個類,該類將直接映射到登陸事件,包含其邏輯——隱藏密碼字段,並根據須要格式化時間戳。

雖然這種方法可行,並且看起來是個不錯的選擇,可是隨着時間的推移,想要擴展咱們的系統,就會發現一些問題:

  • 類太多:隨着事件數量的增長,序列化類的數量將以相同的數量級增加,由於它們是一一映射的。
  • 解決方案不夠靈活:若是須要重用組件的一部分(例如,咱們須要在另外一種事件中隱藏密碼),則必須將其提取到一個函數中,還要從多個類中重複調用它,這意味着咱們沒有作到代碼重用。
  • Boilerplate:serialize()方法必須出如今全部事件類中,調用相同的代碼。雖然咱們能夠將其提取到另外一個類中(建立mixin),但它彷佛不是繼承利用的好方式( Although we can extract this into another class (creating a mixin), it does not seem like a good use of inheritance.)。

另外一種解決方案是,給定一組過濾器(轉換函數)和一個事件實例,可以動態構造對象,該對象可以經過濾器對其字段序列化。而後,咱們只須要定義轉換每種類型的字段的函數,而且經過組合這些函數中的許多函數來建立序列化程序。

一旦有了這個對象,咱們就能夠裝飾類,以便添加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
複製代碼

待續。。。

相關文章
相關標籤/搜索