Python進階: 通過實例詳解裝飾器
Python中的裝飾器有很多用處,比如輸出日志、參數檢查、代理設置、計數計時、結果緩存等等。本文就通過幾個裝飾器例子,詳細解釋一下Python中裝飾器的用法。
-
一步步從簡到繁學習裝飾器用法
-
其他一些裝飾器實例
-
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 0 < 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
來自:https://zhuanlan.zhihu.com/p/23510985