python裝飾器詳細剖析


  裝飾器函數其實是這樣一個接口約束,它必須接受一個 callable 對象作爲參數,然後返回一個 callable 對象,其作用就是爲已經存在的函數或對象添加額外的功能。

函數裝飾器

基本函數裝飾器

  Talk is cheap, show me the code.所以,下面先給出一個最簡單的例子,再來解釋裝飾器原理。

def log_it(func):
    def wrapper(*args, **kwargs):
        print("[Debug]: enter function {}()".format(func.__name__))
        return func(*args, **kwargs)  # if function func has return, remember to return

    return wrapper


@log_it
def add_number(*args):
    return sum(args)


print(add_number(1, 2, 3, 4))
print(add_number.__name__)

# 輸出
[Debug]: enter function add_number()
10
wrapper

  細心的你會發現,print(add_number.__name__)輸出的是wrapper,意味着裝飾器的實際意思就是 add_number = log_it(add_number),所以執行add_number()函數就相當於執行wrapper()函數,而這也就是裝飾器的原理啦。即通過傳入一個函數將其包裝成一個新的函數,賦予它額外的功能。

傳參函數裝飾器

  裝飾器還有更大的靈活性,例如帶參數的裝飾器,比如上面的例子中,你想控制print語句的內容是動態的,那麼你可以給它傳入參數,代碼如下:

def log_it2(level='Debug'):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            print("[{level}]: enter function {func}()".format(
                level=level,
                func=func.__name__))
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper


@log_it2(level='Info')
def add_number2(*args):
    return sum(args)


print(add_number2(1, 2, 3, 4, 5))

# 輸出
[Info]: enter function add_number2()
15

  看到這裏我估計有朋友要暈了,因爲這個裝飾器居然嵌套了三個函數定義,什麼鬼?是不是瞬間感覺還不如學習C++,根本不存在函數嵌套,哈哈~~接下來來仔細分析一下:

  1. 首先我們要清楚相比第一個例子,我們多加了一層函數嵌套,且這一層函數嵌套就是也僅僅是爲了給裝飾器傳遞參數,那麼不難想到add_number2.__name__應該是inner_wrapper(不信你可以輸出看一看);
  2. 基於第1點,我們應該可以推理出它的實際調用是 add_number2 = log_it2(debug='info')(add_number2)

, 從而說明add_number2.__name__的值就是inner_wrapper.

類裝飾器

 類裝飾器和函數裝飾器一樣,包括基本類裝飾器和傳參類裝飾器兩種類型,但是其形式略有不同,類裝飾器必須實現魔法方法__call__函數,關於__call__函數以及更多魔法方法的用法請參考Python面向對象、魔法方法

基本類裝飾器

  示例代碼如下:

class LogIt(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("[DEBUG]: enter function {func}()".format(
            func=self.func.__name__))
        return self.func(*args, **kwargs)


@LogIt
def add_number(*args):
    return sum(args)


print(add_number(1, 2, 3, 4))

  我上面說形式上略有不同,可這一看,好像卻是大有不同,這又是怎麼實現的呢?首先LogIt類實現了構造方法__init__(self,func),它接收一個函數,這看起來很正常;但是,它還是實現了__call__方法並返回了函數func,這不正是我們開篇說的,接受一個函數並返回一個函數嗎?這就是它的實現原理。我們可以驗證一下:

>>>add_number
<__main__.LogIt object at 0x10e4dd7f0>

  看上面的輸出,add_number變成一個函數了,所以我們不難想象,它實際過程是 add_number = LogIt(add_number),如此一來其自然而然就是LogIt的一個實例了。

傳參類裝飾器

 那麼類裝飾器要怎麼傳遞參數呢?請看下面的示例代碼:

class LogIt2(object):
    def __init__(self, level='INFO'):
        self.level = level

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print("[{level}]: enter function {func}()".format(
                level=self.level,
                func=func.__name__))
            return func(*args, **kwargs)
        return wrapper


@LogIt2(level='INFO')
def add_number2(*args):
    return sum(args)


print(add_number2(1, 2, 3, 4, 5))

  如果你融會貫通了上面所蘊含的思想,應該不難推理出它的包裝過程就是:add_number2 = LogIt('INFO')(add_number2),所以add_number2此時應該是實際上是wrapper函數。怎麼知道我說的是對的呢?

>>> add_number2.__name__
wrapper

  以上就是裝飾器的一些基本使用了,多看幾遍,多實踐幾次,你一定可以看懂!

裝飾器執行順序

  以上我們只討論了一個裝飾器的使用,如果同時使用多個裝飾器,會是怎樣的執行流程呢?我們先直接上一個例子,看一看:

def decorator_a(func):
    print('Get in decorator_a')

    def inner_a(*args, **kwargs):
        print('Get in inner_a')
        return func(*args, **kwargs)

    return inner_a


def decorator_b(func):
    print('Get in decorator_b')

    def inner_b(*args, **kwargs):
        print('Get in inner_b')
        return func(*args, **kwargs)

    return inner_b


@decorator_b
@decorator_a
def f(x):
    print('Get in f')
    return x * 2


res = f(1)
print('res:', res)

# 輸出結果
# Get in decorator_a
# Get in decorator_b
# Get in inner_b
# Get in inner_a
# Get in f
# res: 2

  如果你是第一次看裝飾器,且你的預想中也是這個輸出,那麼恭喜你,你一定是996ICU友情鏈接的天選之人;如果你看的目瞪口呆,其實也不要緊,只要你有995ICU友情鏈接2的精神,你也可以是王者。好吧,回到正題,其實關於多裝飾器執行順序的規則就是從裏到外順序執行,即最先調用最裏層的裝飾器,最後調用最外層的裝飾器。其調用過程爲:

f = decorator_b(decorator_a(f))

且最後f的形式爲:

# 注意下面不是可執行代碼,縮進是爲了表示邏輯關係
def inner_b(*args, **kwargs):
  print('Get in inner_b')
  	# -----------inner_a-------------
  	print('Get in inner_a')
  		# -----------f-------------
  		print('Get in f')
  		return x * 2

  我覺得,如果你看懂了上面這一段"僞代碼",那麼你就真的理解了裝飾器原理。如果沒看懂,不要緊,試着多看幾遍就好;就好比心情不好,那就去吃一頓火鍋,如果不夠,那就再來一頓!

內置裝飾器

  在python中有以下幾個常見的內置裝飾器,它們都和python面向對象編程有關,下面分別做簡要介紹:

@abstractmethod

  這是python中抽象類"虛方法"的定義方式,一個類中如果存在@abc.abstractmethod裝飾的方法,那麼其不可以實例化對象,且繼承的子類必須實現@abc.abstractmethod裝飾的方法。

import abc
class A(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def load(self, _input):
        pass

    @abc.abstractmethod
    def save(self, output, data):
        pass


class B(A):
    def load(self, _input):
        return _input.read()

    def save(self, output, data):
        return output.write(data)


if __name__ == '__main__':
    print(issubclass(B, A))
    print(isinstance(B(), A))
    print(A.__subclasses__())
    
# 輸出
True
True
[<class '__main__.B'>]

@property

  類屬性有三個裝飾器: setter , getter , deleter,它們都是在 property () 的基礎上做了一些封裝,其中getter 裝飾器和不帶 getter 的屬性裝飾器效果是一樣的。該特性最重要的功能之一就是能實現屬性的參數檢驗。

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value
    @score.deleter
    def score(self):
        del self._score

s = Student()
s.score = 10
print(s.score)

@classmethod

  被@classmethod裝飾的函數不需要實例化,不需要 self 參數,但第一個參數需要是表示自身類的 cls 參數,可以來調用類的屬性,類的方法,實例化對象等。

class A(object):

    # 屬性默認爲類屬性(可以給直接被類本身調用)
    num = "類屬性"

    # 實例化方法(必須實例化類之後才能被調用)
    def func1(self): # self : 表示實例化類後的地址id
        print("func1")
        print(self)

    # 類方法(不需要實例化類就可以被類本身調用)
    @classmethod
    def func2(cls):  # cls : 表示沒用被實例化的類本身
        print("func2")
        print(cls)
        print(cls.num)
        cls().func1()

    # 不傳遞傳遞默認self參數的方法(該方法也是可以直接被類調用的,但是這樣做不標準)
    def func3():
        print("func3")
        print(A.num) # 屬性是可以直接用類本身調用的
    
# A.func1() 這樣調用是會報錯:因爲func1()調用時需要默認傳遞實例化類後的地址id參數,如果不實例化類是無法調用的
A.func2()
A.func3()

@staticmethod

  被@staticmethod修飾的方法是靜態方法,靜態方法的參數可以根據業務需求傳入,沒有固定參數;然而前面的實例化方法第一個參數必須是self,類方法第一個參數必須是cls。靜態方法,跟普通函數沒什麼區別,與類和實例都沒有所謂的綁定關係,它只不過是碰巧存在類中的一個函數而已,不論是通過類還是實例都可以引用該方法。之所以將其放在類中,是因爲該方法僅爲這個類服務。

class Method(object):

    def __init__(self, data):
        pass

    @staticmethod
    def static_method():
        print "This is static method in class Method"

內置裝飾器小結

  爲了更好的理解辨明實例方法、類方法、靜態方法、抽象方法,我再舉一個例子,從本質上來理一理它們之間的關係:

import abc
class ICU996():

    @staticmethod
    def static_m(self):
        pass
        
    @classmethod
    def class_m(cls):
        pass

    def instance_m(self):
        pass

    @abc.abstractmethod
    def abstract_m(self):
        pass


>>>icu = ICU996()
>>>icu.static_m
<function ICU996.static_m at 0x11af930d0>
>>>icu.class_m
<bound method ICU996.class_m of <class 'test.ICU996'>>
>>>icu.instance_m
<bound method ICU996.instance_m of <test.ICU996 object at 0x11af54978>>
>>>icu.abstract_m
<bound method ICU996.abstract_m of <test.ICU996 object at 0x11af54978>>

  從上面的輸出結果不難看出,實例化方法和抽象方法本質上是一樣的,都是綁定在實例上的方法;而類方法這是綁定在類上的方法;靜態方法則實質上就是一個不同函數。

裝飾器屬性還原

  最後,我們來解決前面的出現的一個問題,在第一個實例中,我們發現被裝飾的函數add_number.__name__變成了wrapper,那麼如果我們並不想讓這樣的事情發生,我們該怎麼做呢?其實,這一點,可以用內置的裝飾器wraps來實現:

from functools import wraps


def log_it(func):
    @wraps(func)  # comment this line to see the diff
    def wrapper(*args, **kwargs):
        print("[Debug]: enter function {}()".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper


@log_it
def add_number(*args):
    """
    add numbers in tuple
    :param args: should be numbers
    :return: the sum of tuple
    """
    return sum(args)


print(add_number.__name__, add_number.__doc__)
# 輸出
add_number 
    add numbers in tuple
    :param args: should be numbers
    :return: the sum of tuple

 它不僅可以使函數名保持不變,還可以保持函數原有的doc string。好,那麼是不是應該思考一下它是怎樣實現的呢?

from functools import partial

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)


def update_wrapper(wrapper,
                   wrapped,
                   assigned=WRAPPER_ASSIGNMENTS,
                   updated=WRAPPER_UPDATES):
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
    # from the wrapped function when updating __dict__
    wrapper.__wrapped__ = wrapped
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper


def wraps(wrapped,
          assigned=WRAPPER_ASSIGNMENTS,
          updated=WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

  這是python官方的實現方式,關於__module__,__name__等特殊屬性,請參考python特殊屬性、魔法方法;這裏包裝的過程簡化來看就是add_number = update_wrapper(wrapper, add_number),其中update_wrapper的功能是更新wrapper函數的一些特殊屬性。

寫在篇後

  裝飾器是python的一大難點,它本質上就是一個函數,它可以讓其他函數在不需要變動的情況下增加額外的功能。裝飾器常用於有切面的應用場景,如插入日誌、性能測試等場景。多看、多試、多用,裝飾器,其實也不難。

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