python装饰器详细剖析
文章目錄
- 函數裝飾器
- 基本函數裝飾器
- 傳參函數裝飾器
- 類裝飾器
- 基本類裝飾器
- 傳參類裝飾器
- 裝飾器執行順序
- 內置裝飾器
- @abstractmethod
- @property
- @classmethod
- @staticmethod
- 內置裝飾器小結
- 裝飾器屬性還原
- 寫在篇后
??裝飾器函數其實是這樣一個接口約束,它 必須接受一個 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 returnreturn 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_wrapperreturn 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++,根本不存在函數嵌套,哈哈~~接下來來仔細分析一下:
, 從而說明add_number2.__name__的值就是inner_wrapper.
類裝飾器
?類裝飾器和函數裝飾器一樣,包括基本類裝飾器和傳參類裝飾器兩種類型,但是其形式略有不同,類裝飾器必須實現魔法方法__call__函數,關于__call__函數以及更多魔法方法的用法請參考Python面向對象、魔法方法
基本類裝飾器
??示例代碼如下:
class LogIt(object):def __init__(self, func):self.func = funcdef __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 = leveldef __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_adef 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 * 2res = 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??我覺得,如果你看懂了上面這一段"偽代碼",那么你就真的理解了裝飾器原理。如果沒看懂,不要緊,試著多看幾遍就好;就好比心情不好,那就去吃一頓火鍋,如果不夠,那就再來一頓!
內置裝飾器
??非常抱歉,本人才疏學淺,此處存在錯誤,經過嚴肅探究,發現property、classmethod、staticmethod不是裝飾器,只是使用形式類似裝飾器,實際上是描述器,文章其他部分應該沒有問題。關于這三個描述器的理解,請見python描述器深度理解
??在python中有以下幾個常見的內置裝飾器,它們都和python面向對象編程有關,下面分別做簡要介紹:
@abstractmethod
??這是python中抽象類"虛方法"的定義方式,一個類中如果存在@abc.abstractmethod裝飾的方法,那么其不可以實例化對象,且繼承的子類必須實現@abc.abstractmethod裝飾的方法。
import abc class A(object):__metaclass__ = abc.ABCMeta@abc.abstractmethoddef load(self, _input):pass@abc.abstractmethoddef save(self, output, data):passclass 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):@propertydef score(self):return self._score@score.setterdef 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.deleterdef score(self):del self._scores = Student() s.score = 10 print(s.score)@classmethod
??被@classmethod裝飾的函數不需要實例化,不需要 self 參數,但第一個參數需要是表示自身類的 cls 參數,可以來調用類的屬性,類的方法,實例化對象等。
class A(object):# 屬性默認為類屬性(可以給直接被類本身調用)num = "類屬性"# 實例化方法(必須實例化類之后才能被調用)def func1(self): # self : 表示實例化類后的地址idprint("func1")print(self)# 類方法(不需要實例化類就可以被類本身調用)@classmethoddef 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@staticmethoddef static_method():print "This is static method in class Method"內置裝飾器小結
??為了更好的理解辨明實例方法、類方法、靜態方法、抽象方法,我再舉一個例子,從本質上來理一理它們之間的關系:
import abc class ICU996():@staticmethoddef static_m(self):pass@classmethoddef class_m(cls):passdef instance_m(self):pass@abc.abstractmethoddef 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 wrapsdef log_it(func):@wraps(func) # comment this line to see the diffdef 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 partialWRAPPER_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:passelse: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 wrapperdef 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的一大難點,它本質上就是一個函數,它可以讓其他函數在不需要變動的情況下增加額外的功能。裝飾器常用于有切面的應用場景,如插入日志、性能測試等場景。多看、多試、多用,裝飾器,其實也不難。
總結
以上是生活随笔為你收集整理的python装饰器详细剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python面向对象、魔法方法
- 下一篇: websocket python爬虫_p