python裝飾器和橫切關注點,裝飾器設計模式

橫切關注點指的是一些具有橫越多個模塊的行爲,使用傳統的軟件開發方法不能夠達到有效的模塊化的一類特殊關注點。

裝飾器提供了不用和繼承結構綁定的定義功能的方法。可以用裝飾器實現程序中的某個方面,讓後將裝飾器應用於類、方法或者函數。

橫切關注點的設計通常見於日誌、審計和安全相關。因爲這些行爲需要橫跨多個模塊。我們可以使用不同的裝飾器來實現不同的橫切需求。

內置裝飾器

常用的用於標註類方法的內置裝飾器有 @property 、 @classmethod 、@staticmethod。

@property用於方法上時,會額外創建一些屬性。他們可以控制賦值、刪除和獲取的動作。

@classmethod 和 @staticmethod可以將一個方法函數轉換成一個類級函數。被裝飾的方法可以用類調用,而不是實例對象。被@classmethod 裝飾的方法第一個傳入的參數時當前的類,而@staticmethod沒有顯示的引用。

class Area:
    @staticmethod
    def from_width_height(w,h):
        return Area(w,h)
    def __new__(cls,w,h):
        self = super().__new__(cls)
        self._area = w*h
        return self
    @property
    def area(self):
        return self._area

        
>> a = Area.from_width_heght(10,20)
>> a.area
200

標準庫中也提供了很多裝飾器。比如contextlib、functools、unittest等模塊都包含了橫切方面的經典範例裝飾器

例如functools中的 @total_ordering裝飾器,它定義了一些列比較運算符,可以讓我們不用定義全部的比較運算函數就可以實現一套完整的比較運算。

import functools

@functools.total_ordering
class Person:
    def __init__(self,age):
        self.age = age
        
    def __eq__(self, other):
        return self.age == other.age
        
    def __lt__(self,other):
        return self.age < other.age
    
>> xiaoming = Person(10)
>> xiaohong = Person(20)
>> xiaoming == xiaohong
Out[33]: False
>> xiaoming > xiaohong
Out[34]: False
>> xiaoming <= xiaohong
Out[36]: True

mixin類

如果使用多重繼承來創建橫切面,可以使用一個基類加上一個mixin類的方式來引入新的功能。通常使用mixin類來創建橫切面。

mixin類實際上是抽象基類,其中實現了一些通用的方法,使用mixin類可以使代碼易讀並且使橫切面以一致的方式定義。

比如我們想要實現一個上下文管理器,那麼可以使用 contextlib 中的 ContextDecorator mixin。

上下文管理器即是實現了 __enter__和__exit__方法的類 ,可以使用 with 語句來管理上下文。

這個例子僅爲理解。

from contextlib import ContextDecorator
class Person(ContextDecorator):
    def __init__(self,age,name):
        self.age = age
        self.name = name
    def __enter__(self):
        return self.__dict__
    def __exit__(self, exc_type, exc_value, traceback):
        pass
    
>> with Person(10,'xiaoming') as d:
>>     print(d)
{'age': 10, 'name': 'xiaoming'}

創建裝飾器

函數裝飾器

當一個函數被裝飾過後,原有的函數名和doc會被裝飾器中的相應信息所替代。要避免這個問題,可以使用functools.wraps裝飾器。

假設我們有一些函數,需要在執行前打印參數信息,執行後打印結果信息,那麼正常代碼是這樣的:

loggin.debug("function(",args,kw,")")
result = func(*args,**Kw)
loggin.debug("result="result)
return result

如果有很多這樣的函數,使用這樣的方式會出現很多重複的代碼,並且會讓邏輯不清晰,使用裝飾器可以很好的解決這個問題。

import functools
import logging
def debug(func):
    @functools.wraps(func)
    def logged(*args,**kwargs):
        # 爲每個函數使用特定的日誌記錄器
        log = logging.getLogger(func.__name__)
        log.debug("%s(%r, %r)",func.__name__,args,kwargs)
        result = func(*args,**kwargs)
        log.debug("%s = %r",func.__name__,result)
        return result
    return logged

@debug
def my_sum(x,y):
    return x+y

帶參數的裝飾器

帶參數的裝飾器意在修改外邊的封裝函數。

@decorator(arg)
def func():
    pass

# 上面的代碼的正常版本是這樣的
def func():
    pass
func = decorator(arg)(func)

所以帶參數的裝飾器是這樣的結構:

def decorator(config):
    def concrete_decorator(func):
        @functools.wraps(func)
        def wrapped(*args,**kwargs):
            return func(*args,**kwargs)
        return wrapped
    return concrete_decorator

下面是一個上面日誌裝飾器的進階版,支持設置日誌記錄器名稱:

def debug_named(log_name):
    def concrete_decorator(func):
    	@functools.wraps(func)
    	def wrapped(*args,**kwargs):
        	# 爲每個函數使用特定的日誌記錄器
        	log = logging.getLogger(log_name)
        	log.debug("%s(%r, %r)",func.__name__,args,kwargs)
        	result = func(*args,**kwargs)
        	log.debug("%s = %r",func.__name__,result)
        	return result
        return wrapped
    return concrete_decorator

方法函數裝飾器

方法函數裝飾器和單獨的函數裝飾器是一樣的,只是在不同的上下文中使用,所以需要顯式的聲明self。

假設我們將上面的函數用作與方法函數上,只需要進行一點更改:

def debug(func):
    @functools.wraps(func)
    # 添加self
    def logged(self,*args,**kwargs):
        # 爲每個函數使用特定的日誌記錄器
        log = logging.getLogger(func.__name__)
        log.debug("%s(%r, %r)",func.__name__,args,kwargs)
        # 添加self
        result = func(self,*args,**kwargs)
        log.debug("%s = %r",func.__name__,result)
        return result
    return logged

類裝飾器

和函數裝飾器類似,也可以寫一個類裝飾器,用來添加功能。他接受一個類作爲參數,返回一個類作爲返回值。

從技術上說,創建一個封裝了原始類的類是可以的,但包裝類本身必須是非常通用的;也可以創建一個被裝飾類的子類,但是這樣會導致使用者非常困惑;而刪除類中的一些功能是最不可取的做法。

@functools.total_ordering裝飾器是創建lambda對象並將他們賦值給類屬性。

假如我們現在希望每個類有自己的日誌記錄器,那麼我們會這麼做:

class MyClass:
    def __init__(self):
        self.logger = logging.getLogger(self.__class__.__qualname__)

這樣的作法缺點是他創建了一個對象,他不屬於類操作的一部分但卻是類的一個獨立方面的logger實例。這樣的額外方法會污染類,並且每次實例化時都會有額外的消耗。

我們可以把logger從實例方法中拿出來,放到類屬性中,這樣可以避免每次實例化的消耗:

class MyClass:
    logger = logging.getLogger('MyClass')
    def __init__(self):
        self.logger.info('start...')

但這樣依舊缺少靈活性。因爲logger在類級屬性中,這時類還沒有被創建,無法獲取到我們需要的類名信息(self.__class__.__qualname__),沒辦法自動設置類名。

但是用類裝飾器可以解決這個問題:

def logged(class_):
    class_.logger = logging.getLogger(class_.__qualname__)
    return class_

@logged
class C:
    def __init__(self):
        self.logger.info('start...')

假設我們現在需要通過裝飾器向類中創建新的方法函數,那麼實際上是分爲兩步的:

  1. 創建方法
  2. 將他插入到類中

通常情況下,mixin類比裝飾器更好用。但是存在特殊情況,比如total_ordering裝飾器,它可以根據類中已經提供了什麼方法,來靈活的插入方法。

假如我們想定義一個標準的功能,並把這個功能添加到多個不同的類中,那麼用裝飾器的方法可以這樣:

# 裝飾器即閉包,閉包是一個獨立命名空間,所以下面的這種命名方式不會導致命名衝突

def print_class_name(class_):
    def print_class_name(self):
        print "class name is {0.__class__.__qualname__}".format(self)
    class_.print_class_name = print_class_name
    return class_

@print_class_name
class C:
    pass

但是這樣有個問題,我們不能通過重載print_class_name()方法來處理特殊的情況。如果我們要擴展它,那麼必須要將他升級爲可調用對象。

如果我們決定要升級它,那麼倒不如使用一個mixin類向類中添加方法。這是使用繼承的機制,所以我們可以靈活的更改他。

class PrintClassName:
    def print_class_name(self):
        print "class name is {0.__class__.__qualname__}".format(self)

class C(PrintClassName):
    pass

裝飾器和mixin的目的是爲了分離業務相關的功能和通用的功能,例如安全、審計、日誌。這其實也是一種設計模式。

繼承和不屬於繼承的橫切關注點(裝飾器和mixin),在設計上是有卻別的:

  1. 繼承的功能是顯式設計的一部分,他們是實現業務的一部分
  2. mixin或裝飾器他們是定義一個對象如何工作,實現的是通用功能的部分。

繼承和橫切關注點(裝飾器和mixin)之間的分別有時並不明顯。由於大家看待問題的切入點不同,在一些情況下通常是根據個人的習慣或者審美來決定的。

方法通常是從類中定義創建的,他們是主類或者mixin類的一部分。我們可以通過封裝來引入新的功能,但在一些情況下,我們會發現需要暴露一些只是簡單委託給底層類的方法,這種情況使用裝飾器或者mixin可能是更好的選擇。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章