python 裝飾器總結
裝飾器通常是一個命名的對象(不允許 lambda 表達式), 在被(裝飾函數)調用時接受單一參數, 並返回另一個可調用對象. 這裏的可調用對象, 不僅僅包含函數和方法, 還包括類. 任何可調用對象(任何實現了 call 方法的對象都是可調用的)都可用作裝飾器, 他們返回的對象也不是簡單的函數, 而是實現了自己的 call 方法的更復雜的類實例.
@some_decorator def decorated_function(): pass # 以上寫法總是可以替換為顯式的裝飾器調用和函數的重新賦值: decorated_function = some_decorator(decorated_function)
1. 裝飾器定義/使用方法
1.1 通用模式: 作為一個函數
def mydecorator(function):
def wrapped(*args, **kwargs):
# 在函數調用之前, 做點什麽
result = function(*args, **kwargs)
# 在函數調用之後, 做點什麽
# 返回結果
return result
# 返回 wrapper 作為裝飾函數
return wrapped
1.2 實現 call 方法: 作為一個類
非參數化裝飾器用作類的通用模式如下:
class DecoratorAsClass:
def __init__(self, function):
self.function = function
def __call__(self, *args, **kw):
# 在調用原始函數之前, 做點什麽
result = self.function(*args, **kwargs)
# 在調用原始函數之後, 做點什麽
# 返回結果
return result
1.3 參數化裝飾器 : 實現第二層包裝
def repeat(number=3): """ 多次重復執行裝飾函數, 返回最後一次原始函數調用的值作為結果. : param number: 重復次數, 默認值為 3 """ def actual_decorator(function): def wrapped(*args, **kwargs): result = None for _ in range(number): result = function(*args, **kwargs) return result return wrapped return actual_decorator @repeat(2) def foo(): print("foo")
帶參數的裝飾器總是可以做如下裝換:
foo = repeat(number=3)(foo)
即使參數化裝飾器的參數有默認值, 但名字後面也必須加括號
@repeat()
def bar():
print("bar")
1.4 保存內省的裝飾器
使用裝飾器的常見缺點是: 使用裝飾器時, 不保存函數元數據(主要是文檔字符串和原始函數名). 裝飾器組合創建了一個新函數, 並返回一個新對象, 完全沒有考慮原函數的標誌. 這將導致調試裝飾器裝飾過的函數更加困難, 也會破壞可能用到的大多數自動生產文檔的工具, 應為無法訪問原始的文檔字符串和函數簽名.
解決這個問題的方式, 就是使用 functools
模塊內置的 wraps()
裝飾器.
from functools import wraps
def preserving_decorator(function):
@wraps(function)
def wrapped(*args, **kwargs):
"""包裝函數內部文檔"""
return function(*args, **kwargs)
return wrapped
@preserving_decorator
def function_with_important_docstring():
"""這是我們想要保存的文檔字符串"""
pass
print(function_with_important_docstring.__name__)
print(function_with_important_docstring.__doc__)
2. 裝飾器常用示例
2.1 參數檢查
檢查函數接受或返回的參數, 在特定上下文中執行時可能有用.
# 裝飾器代碼
rpc_info = {} # 在實際讀取時, 這個類定義會填充 rpc_info 字典, 並用於檢查參數類型的特定環境中.
def xmlrpc(in_=(), out=(type(None), )):
def _xmlrpc(function):
# 註冊簽名
func_name = function.__name__
rpc_info[func_name] = (in_, out)
def _check_types(elements, types):
"""用來檢查類型的子函數"""
if len(elements) != len(types):
raise TypeError("Argumen count is wrong")
typed = enumerate(zip(elements, types))
for index, couple in typed:
arg, of_the_right_type = couple
if isinstance(arg, of_the_right_type):
continue
raise TypeError("Arg #%d should be %s" % (index, of_the_right_type))
def __xmlrpc(*args): # 沒有允許的關鍵詞
# 檢查輸入的內容
if function.__class__ == "method":
checkable_args = args[1:] # 類方法, 去掉 self
else:
checkable_args = args[:] # 普通函數
_check_types(checkable_args, in_)
# 運行函數
res = function(*args)
# 檢查輸入內容
if not type(res) in (tuple, list):
checkable_res = (res, )
else:
checkable_res = res
_check_types(checkable_res, out)
# 函數機器類型檢查成功
return res
return __xmlrpc
return _xmlrpc
# 使用示例
class RPCView:
@xmlrpc((int, int)) # two int --> None
def meth1(self, int1, int2):
print("received %d and %d" % (int1, int2))
@xmlrpc((str, ), (int, )) # string --> int
def meth2(self, phrase):
print("received %s" % phrase)
return 12
# 調用輸出
print(rpc_info)
# 輸出:
# {'meth1': ((<class 'int'>, <class 'int'>), (<class 'NoneType'>,)), 'meth2': ((<class 'str'>,), (<class 'int'>,))}
my = RPCView()
my.meth1(1, 2)
# 輸出: 類型檢查成功
# received 1 and 2
my.meth2(2)
# 輸出: 類型檢查失敗
# File "D:\VBoxShare\Work\Documents\PyProject\PyCookbook\test.py", line 57, in <module>
# my.meth2(2)
# File "D:\VBoxShare\Work\Documents\PyProject\PyCookbook\test.py", line 25, in __xmlrpc
# _check_types(checkable_args, in_)
# File "D:\VBoxShare\Work\Documents\PyProject\PyCookbook\test.py", line 20, in _check_types
# raise TypeError("Arg #%d should be %s" % (index, of_the_right_type))
# TypeError: Arg #0 should be <class 'str'>
2.2 緩存
緩存裝飾器與參數檢查十分相似, 不過他重點是關註那些內容狀態不會影響輸入的函數, 每組參數都可以鏈接到唯一的結果. 因此, 緩存裝飾器可以將輸出與計算法所需的參數放在一起, 並在後續的調用中直接返回他(這種行為成為 memoizing).
import time
import hashlib
import pickle
cache = {}
def is_obsolete(entry, duration):
return time.time() - entry["time"] > duration
def compute_key(function, args, kw):
""" 利用已排序的參數來構建 SHA 哈希鍵, 並將結果保存在一個全局字典中.
利用 pickle 來建立 hash , 這是凍結所有作為參數傳入的對象狀態的快捷方式, 以確保所有參數都滿足於要求.
"""
key = pickle.dumps((function.__name__, args, kw))
return hashlib.sha1(key).hexdigest()
def memoize(duration=10):
def _memoize(function):
def __memoize(*args, **kw):
key = compute_key(function, args, kw)
# 是否已經擁有它了?
if (key in cache and not is_obsolete(cache[key], duration)):
print("We got a winner.")
return cache[key]["value"]
# 計算
result = function(*args, **kw)
# 保存結果
cache[key] = {
"value": result,
"time": time.time()
}
return result
return __memoize
return _memoize
@memoize()
def func_1(a, b):
return a + b
print(func_1(2, 2)) # 4
print(func_1(2, 2)) # print , 4
@memoize(1)
def func_2(a, b):
return a + b
print(func_2(2, 2)) # 4
time.sleep(1)
print(func_2(2, 2)) # 4
緩存值還可以與函數本身綁定, 以管理其作用域和生命周期, 代替集中化的字典. 但在任何情況下, 更高效的裝飾器會使用基於高級緩存算法的專用緩存庫.
2.3 代理
代理裝飾器使用全局代理來標記和註冊函數. 例如, 一個根據當前用戶來保護代碼訪問的安全層可以使用集中式檢查器和相關的可調用對象要求的權限來實現.
class User:
def __init__(self, roles):
self.roles = roles
class Unauthorized(Exception):
pass
def protect(role):
def _protect(function):
def __protect(*args, **kw):
user = globals().get("user")
if user is None or role not in user.roles:
raise Unauthorized("I won't tell you.")
return function(*args, **kw)
return __protect
return _protect
tarek = User(("admin", "user"))
bill = User(("user",))
class MySecrets:
@protect("admin")
def waffle_recipe(self):
print("use tons of butter")
these_are = MySecrets()
user = tarek
these_are.waffle_recipe() # use tons of butter
user = bill
these_are.waffle_recipe() # __main__.Unauthorized: I won't tell you.
以上模型常用於 Python Web 框架中(權限驗證), 用於定義可發布類的安全性. 例如, Django 提供裝飾器來保護函數訪問的安全.
2.4 上下文提供者
上下文裝飾器確保函數可以運行在正確的上下文中, 或者在函數前後運行一些代碼, 換句話說, 他設定並復位一個特定的執行環境.
例如, 當一個數據項需要在多個線程之間共享時, 就要用一個鎖來保護她避免多次訪問, 這個鎖可以在裝飾器中編寫.
from threading import RLock
lock = RLock()
def synchronized(function):
def _synchronized(*args, **kw):
lock.acquire()
try:
return function(*args, **kw)
finally:
lock.release()
return _synchronized
@synchronized
def thread_safe(): # 確保鎖定資源
pass
上下裝飾器通常會被上下文管理器(with) 替代.
python 裝飾器總結