1. 程式人生 > 其它 >Python進階: 通過例項詳解裝飾器(附程式碼)

Python進階: 通過例項詳解裝飾器(附程式碼)

Python中的裝飾器有很多用處,比如輸出日誌、引數檢查、代理設定、計數計時、結果快取等等。本文就通過幾個裝飾器例子,詳細解釋一下Python中裝飾器的用法。

  • 一步步從簡到繁學習裝飾器用法
  • 其他一些裝飾器例項
  • Python中自帶的裝飾器


按照慣例,先上程式碼:GitHub - xianhu/LearnPython: 以擼程式碼的形式學習Python

一步步從簡到繁學習裝飾器用法

(1)最簡單的裝飾器,實現日誌輸出功能:

# 構建裝飾器
def logging(func):
    @functools.wraps(func)
    def decorator():
        print("%s called" % func.__name__)
        result = func()
        print("%s end" % func.__name__)
        return result
    return decorator

# 使用裝飾器
@logging
def test01():
    return 1

# 測試用例
print(test01())
print(test01.__name__)

程式碼很簡單,很容易看懂。這裡注意"functools.wraps"用法,其目的是"test01.__name__"輸出正確的"test01"。"@logging"相當於"test01 = logging(test01)",返回的是decorator函式,所以如果不加"functools.wraps",則"test01.__name__"返回為"decorator"。

注意,此時test01沒有引數,對於帶有引數的函式,logging裝飾器則不再適用。那麼如果想裝飾帶有引數的函式,裝飾器該怎麼寫呢?

(2)裝飾器傳入函式引數,並正確返回結果:

# 構建裝飾器
def logging(func):
    @functools.wraps(func)
    def decorator(a, b):
        print("%s called" % func.__name__)
        result = func(a, b)
        print("%s end" % func.__name__)
        return result
    return decorator

# 使用裝飾器
@logging
def test01(a, b):
    print("in function test01, a=%s, b=%s" % (a, b))
    return 1

# 測試用例
print(test01(1, 2))

這裡的test01函式帶有引數a、b,那麼decorator函式帶有同樣的引數即可。那麼問題又來了,如何讓logging裝飾器更加通用,而不是隻裝飾引數為兩個的函式呢?這時候自然想到Python中的 * 和 ** 的用法。

# 構建裝飾器
def logging(func):
    @functools.wraps(func)
    def decorator(*args, **kwargs):
        print("%s called" % func.__name__)
        result = func(*args, **kwargs)
        print("%s end" % func.__name__)
        return result
    return decorator

# 使用裝飾器
@logging
def test01(a, b):
    print("in function test01, a=%s, b=%s" % (a, b))
    return 1

# 使用裝飾器
@logging
def test02(a, b, c=1):
    print("in function test02, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 測試用例
print(test01(1, 2))
print(test02(1, 2, c=3, d=4))

此時對於任意引數的函式,logging都可以進行裝飾。但是注意,logging裝飾器是不帶引數的,那麼裝飾器可以帶引數嗎?當然可以,我們換個例子:引數檢查。

(3)構建帶有引數的裝飾器,並正確返回結果:

# 構建裝飾器
def params_chack(a_type, b_type):
    def _outer(func):
        @functools.wraps(func)
        def _inner(a, b):
            assert isinstance(a, a_type) and isinstance(b, b_type)
            return func(a, b)
        return _inner
    return _outer

# 使用裝飾器
@params_chack(int, (list, tuple))
def test03(a, b):
    print("in function test03, a=%s, b=%s" % (a, b))
    return 1

# 測試用例
print(test03(1, [2, 3]))   # 引數正確
print(test03(1, 2))        # 引數錯誤

從程式碼可以看出,實際上就是在原有裝飾器的基礎上,外層又加了一層包裝。params_check裝飾器的作用是限制第一個引數為a_type,第二個引數為b_type。類似於(2),這裡如何讓裝飾器更加通用,而不是隻裝飾引數為兩個的函式呢?這裡又一次想到Python中的 * 和 **。

# 構建裝飾器
def params_chack(*types, **kwtypes):
    def _outer(func):
        @functools.wraps(func)
        def _inner(*args, **kwargs):
            result = [isinstance(_param, _type) for _param, _type in zip(args, types)]
            assert all(result), "params_chack: invalid parameters"
            result = [isinstance(kwargs[_param], kwtypes[_param]) for _param in kwargs if _param in kwtypes]
            assert all(result), "params_chack: invalid parameters"
            return func(*args, **kwargs)
        return _inner
    return _outer

# 使用裝飾器
@params_chack(int, str, c=(int, str))
def test04(a, b, c):
    print("in function test04, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 測試用例
print(test04(1, "str", 1))         # 引數正確
print(test04(1, "str", "abc"))     # 引數正確
print(test04("str", 1, "abc"))     # 引數錯誤

此時params_check裝飾器不但能夠傳入任意個數的引數,而且支援K-V形式的引數傳遞。

(4)使用裝飾器裝飾類中的函式,比較簡單,直接看程式碼。注意此時第一個引數為self本身:

# 使用裝飾器
class ATest(object):
    @params_chack(object, int, str)
    def test(self, a, b):
        print("in function test of ATest, a=%s, b=%s" % (a, b))
        return 1

# 測試用例
a_test = ATest()
a_test.test(1, "str")    # 引數正確
a_test.test("str", 1)    # 引數錯誤

(5)多個裝飾器同時裝飾一個函式,也比較簡單,直接看程式碼:

# 使用裝飾器
@logging
@params_chack(int, str, (list, tuple))
def test05(a, b, c):
    print("in function test05, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 測試用例
print(test05(1, "str", [1, 2]))        # 引數正確
print(test05(1, "str", (1, 2)))        # 引數正確
print(test05(1, "str", "str1str2"))    # 引數錯誤

(6)將裝飾器寫為類的形式,即“裝飾器類”。此時對於裝飾器類的要求是必須是可被呼叫的,即必須實現類的__call__方法。直接上程式碼:

# 構建裝飾器類
class Decorator(object):
    def __init__(self, func):
        self.func = func
        return

    def __call__(self, *args, **kwargs):
        print("%s called" % self.func.__name__)
        result = self.func(*args, **kwargs)
        print("%s end" % self.func.__name__)
        return result

# 使用裝飾器
@Decorator
def test06(a, b, c):
    print("in function test06, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 測試用例
print(test06(1, 2, 3))

這裡的裝飾器類的建構函式中傳入func,使其能在__call__方法中被呼叫。同時這裡的裝飾器類並沒有帶有引數,實現不了類似於二手遊戲賬號賣號平臺地圖引數檢查的功能。類似於上邊的思路,我們這裡也可以構建帶有引數的裝飾器類,還是以引數檢查為例:

# 構建裝飾器類
class ParamCheck(object):

    def __init__(self, *types, **kwtypes):
        self.types = types
        self.kwtypes = kwtypes
        return

    def __call__(self, func):
        @functools.wraps(func)
        def _inner(*args, **kwargs):
            result = [isinstance(_param, _type) for _param, _type in zip(args, self.types)]
            assert all(result), "params_chack: invalid parameters"
            result = [isinstance(kwargs[_param], self.kwtypes[_param]) for _param in kwargs if _param in self.kwtypes]
            assert all(result), "params_chack: invalid parameters"
            return func(*args, **kwargs)
        return _inner

# 使用裝飾器
@ParamCheck(int, str, (list, tuple))
def test07(a, b, c):
    print("in function test06, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 測試用例
print(test07(1, "str", [1, 2]))    # 引數正確
print(test07(1, "str", (1, 2)))    # 引數正確
print(test07(1, 2, (1, 2)))        # 引數錯誤

其他一些裝飾器例項

函式快取:一個函式的執行結果可以被快取在記憶體中,下次再次呼叫時,可以先檢視快取中是否存在,如果存在則直接返回快取中的結果,否則返回函式呼叫結果。這種裝飾器比較適合裝飾過程比較複雜或耗時的函式,比如資料庫查詢等。

# 例項: 函式快取
def funccache(func):
    cache = {}

    @functools.wraps(func)
    def _inner(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return _inner

# 使用裝飾器
@funccache
def test08(a, b, c):
    # 其他複雜或耗時計算
    return a + b + c

還有很多其他例子,比如函式呼叫計數、函式計時、函式自動重試等,思路都基本相同,這裡就不一一列舉了。



Python中自帶的裝飾器

Python中自帶有三個和class相關的裝飾器:@staticmethod、@classmethod 和@property。

(1)先看@property,可以將其理解為“將類方法轉化為類屬性的裝飾器”。先看例項:

# 使用Python自帶的裝飾器
class People(object):

    def __init__(self):
        self._name = None
        self._age = None
        return

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, name):
        self._name = name
        return

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, age):
        assert  < age < 120
        self._age = age
        return

p = People()
p.name = "tom"          # 設定name
p.age = 12              # 設定age
print(p.name, p.age)    # 輸出name和age
p.age = 120             # 設定age, 此時認為120為異常資料

這裡定義一個People類,有兩個屬性name和age。當我們聲明瞭例項p,使用p操作name和age時,實際上是呼叫的name、age方法,此時會做引數檢查等工作。@property將name方法轉化為屬性,同時當對該屬性進行賦值時,會自動呼叫@name.setter將下邊的name方法。

@property有.setter、.getter和.deleter三中裝飾器,分別對應賦值、取值和刪除三種操作。

(2)@staticmethod 將類成員方法宣告為類靜態方法,類靜態方法沒有 self 引數,可以通過類名或類例項呼叫。

(3)@classmethod 將類成員方法宣告為類方法,類方法所接收的第一個引數不是self,而是cls,即當前類的具體型別。

靜態方法和類方法都比較簡單,一個簡單的例子解釋靜態方法和類方法:

# 類靜態方法和類方法
class A(object):
    var = 1

    def func(self):
        print(self.var)
        return

    @staticmethod
    def static_func():
        print(A.var)
        return

    @classmethod
    def class_func(cls):
        print(cls.var)
        cls().func()
        return