深入淺出理解Python裝飾器

各位朋友,大家好,我是Payne,歡迎大家關注我的博客,我的博客地址是https://qinyuanpei.github.io。今天我想和大家一起探討的話題是Python中的裝飾器。因為工作關係最近這段時間在頻繁地使用Python,而我漸漸意識到這是一個非常有趣的話題。無論是在Python標準庫還是第三方庫中,我們越來越頻繁地看到裝飾器的身影,從某種程度上而言,Python中的裝飾器是Python進階者的一條必由之路,正確合理地使用裝飾器可以讓我們的開發如虎添翼。裝飾器天然地和函數式編程、設計模式、AOP等概念產生聯繫,這更加讓我對Python中的這個特性產生興趣。所以,在這篇文章中我將帶領大家一起來剖析Python中的裝飾器,希望對大家學習Python有所幫助。

什麼是裝飾器

什麼是裝飾器?這是一個問題。在我的認知中,裝飾器是一種語法糖,其本質就是函數。我們注意到Python具備函數式編程的特徵,譬如lambda演算,map、filter和reduce等高階函數。在函數式編程中,函數是一等公民,即「一切皆函數」。Python的函數式編程特性由早期版本通過漸進式開發而來,所以對「一切皆對象」的Python來說,函數像普通對象一樣使用,這是自然而然的結果。為了驗證這個想法,我們一起來看下面的示例。

函數對象

def square(n): return n * nfunc = squareprint func #<function square at 0x01FF9FB0>print func(5) #25

可以注意到,我們將一個函數直接賦值給一個變數,此時該變數表示的是一個函數對象的實例,什麼叫做函數對象呢?就是說你可以將這個對象像函數一樣使用,所以當它帶括弧和參數時,表示立即調用一個函數;當它不帶括弧和參數時,表示一個函數。在C#中我們有一個相近的概念被稱為委託,而委託本質上是一個函數指針,即表示指向一個方法的引用,從這個角度來看,C#中的委託類似於這裡的函數對象,因為Python是一個動態語言,所以我們可以直接將一個函數賦值給一個對象,而無需藉助Delegate這樣的特殊類型。

* 使用函數作為參數

def sum_square(f,m,n): return f(m) + f(n) print sum_square(square,3,4) #25

  • 使用函數作為返回值

def square_wrapper(): def square(n): return n * n return square wrapper = square_wrapper()print wrapper(5) #25

既然在Python中存在函數對象這樣的類型,可以讓我們像使用普通對象一樣使用函數。那麼,我們自然可以將函數推廣到普通對象適用的所有場合,即考慮讓函數作為參數和返回值,因為普通對象都都具備這樣的能力。為什麼要提到這兩點呢?因為讓函數作為參數和返回值,這不僅是函數式編程中高階函數的基本概念,而且是閉包、匿名方法和lambda等特性的理論基礎。從ES6中的箭頭函數、Promise、React等可以看出,函數式編程在前端開發中越來越流行,而這些概念在原理上是相通的,這或許為我們學習函數式編程提供了一種新的思路。在這個示例中,sumsquare()squarewrapper()兩個函數,分別為我們展示了使用函數作為參數和返回值的可行性。

def outer(m): n = 10 def inner(): return m + n return outerfunc = outer(5)print func() #15#內函數修改外函數局部變數def outer(a): b = [10] def inner(): b[0] += 1 return a + b[0] return innerfunc = outer(5)print func() #16print func() #17

對Python這門語言來說,這裡的outer()函數和inner()函數分別被稱為外函數和內函數,變數n的定義不在inner()函數內部,因此變數n稱為inner()函數的環境變數。在Python中,一個函數及其環境變數就構成了閉包(Closure)。要理解閉包我認為我們可以把握這三點:第一,外函數返回了內函數的引用,即我們調用outer()函數時返回的是inner()函數的引用;第二,外函數將自己的局部變數綁定到內函數,其中變數b的目的是展示如何在內函數中修改環境變數;第三,調用內函數意味著發生出、入棧,不同的是每次調用都共享同一個閉包變數,請參考第二個示例。好了,現在講完閉包以後,我們就可以開始說Python中的裝飾器啦。

裝飾器

裝飾器是一種高級Python語法,裝飾器可以對一個函數、方法或者類進行加工。所以,裝飾器就像女孩子的梳妝盒,經過一番打扮後,可以讓女孩子更漂亮。裝飾器使用起來是非常簡單的,其難點主要在如何去寫一個裝飾器。帶著這個問題,我們來一起看看Python中的裝飾器是如何工作的,以及為什麼我們說裝飾器的本質就是函數。早期的Python中並沒有裝飾器這一語法,最早出在Python 2.5版本中且僅僅支持函數的裝飾,在Python 2.6及以後版本中裝飾器被進一步用於類。

def decorator_print(func): def wrapper(*arg): print arg return func(*arg) return wrapper@decorator_printdef sum(array): return reduce(lambda x,y:x+y,array)data = [1,3,5,7,9]print sum(data)

我們注意到裝飾器可以使用def來定義,裝飾器接收一個函數對象作為參數,並返回一個新的函數對象。裝飾器通過名稱綁定,讓同一個變數名指向一個新返回的函數對象,這樣就達到修改函數對象的目的。在使用裝飾器時,我們通常會在新函數內部調用舊函數,以保留舊函數的功能,這正是「裝飾」一詞的由來。在定義好裝飾器以後,就可以使用@語法了,其實際意義時,將被修飾對象作為參數傳遞給裝飾器函數,然後將裝飾器函數返回的函數對象賦給原來的被修飾對象。裝飾器可以實現代碼的可復用性,即我們可以用同一個裝飾器修飾多個函數,以便實現相同的附加功能。在這個示例中,我們定義了一個decorator_print的裝飾器函數,它負責對一個函數func進行修飾,在調用函數func以前執行print語句,進而可以幫助我們調試函數中的參數,通過@語法可以讓我們使用一個名稱去綁定一個函數對象。在這裡,它的調用過程可以被分解為:

sum = decorator_print(sum)print sum()

接下來,我們再來寫一個統計代碼執行時長的裝飾器decorator_watcher:

def decorator_watcher(func): def wrapper(*arg): t1 = time.time() result = func(*arg) t2 = time.time() print(time:,t2-t1) return result return wrapper

此時我們可以使用該裝飾器來統計sum()函數執行時長:

@decorator_watcherdef sum(array): return reduce(lambda x,y:x+y,array)data = [1,3,5,7,9]print sum(data)

現在,這個裝飾器列印出來的信息格式都是一樣的,我們無法從終端中分辨它對應哪一個函數,因此考慮給它增加參數以提高辨識度:

def decorator_watcher(funcName=): def decorator(func): def wrapper(*arg): t1 = time.time() result = func(*arg) t2 = time.time() print(%s time: % funcName,t2-t1) return result return wrapper return decorator@decorator_watcher(sum)def sum(array): return reduce(lambda x,y:x+y,array)data = [1,3,5,7,9]print sum(data)

裝飾器同樣可以對類進行修飾,譬如我們希望某一個類支持單例模式,在C#中我們定義泛型類Singleton。下面演示如何通過裝飾器來實現這一功能:

instances = {}def getInstance(aClass, *args): if aClass not in instances: instances[aClass] = aClass(*args) return instances[aClass]def singleton(aClass): def onCall(*args): return getInstance(aClass,*args) return onCall@singletonclass Person: def __init__(self,name,hours,rate): self.name = name self.hours = hours self.rate = rate def pay(self): return self.hours * self.rate

除此以外,Python標準庫中提供了諸如classmethod、staticmethod、property等類裝飾器,感興趣的讀者朋友可以自行前去研究,這裡不再贅述。

裝飾器與設計模式

裝飾器可以對函數、方法和類進行修改,同時保證原有功能不受影響。這自然而然地讓我想到面向切面編程(AOP),其核心思想是,以非侵入的方式,在方法執行前後插入代碼片段,以此來增強原有代碼的功能。面向切面編程(AOP)通常通過代理模式(靜態/動態)來實現,而與此同時,在Gof提出的「設計模式」中有一種設計模式被稱為裝飾器模式,這兩種模式的相似性,讓我意識到這會是一個有趣的話題,所以在接下來的部分,我們將討論這兩種設計模式與裝飾器的內在聯繫。

代理模式

代理模式,屬於23種設計模式中的結構型模式,其核心是為真實對象提供一種代理來控制對該對象的訪問。在這裡我們提到了真實對象,這就要首先引出代理模式中的三種角色,即抽象對象代理對象真實對象。其中:

* 抽象對象:通過介面或抽象類聲明真實角色實現的業務方法。

* 代理對象:實現抽象角色,是真實角色的代理,通過真實角色的業務邏輯方法來實現抽象方法。

* 真實對象:實現抽象角色,定義真實角色所要實現的業務邏輯,供代理角色調用。

下面是一個典型的代理模式UML圖示:

通過UML圖我們可以發現,代理模式通過代理對象隱藏了真實對象,實現了調用者對真實對象的訪問控制,即調用者無法直接接觸到真實對象。「代理」這個辭彙是一個非常生活化的辭彙,因為我們可以非常容易地聯繫到生活種的中介這種角色,譬如租賃房屋時會存在房屋中介這種角色,租客(調用者)通過中介(代理對象)來聯繫房東(真實對象),這種模式有什麼好處呢?中介(代理對象)的存在隔離了租客(調用者)與房東(真實對象),有效地保護了房東(真實對象)的個人隱私,使其免除了頻繁被租客(調用者)騷擾的困惑,所以代理模式的強調的是控制

按照代理機制上的不同來劃分,代理模式可以分為靜態代理動態代理。前者是將抽象對象代理對象真實對象這三種角色在編譯時就確定下來。對於C#這樣的靜態強類型語言而言,這意味著我們需要手動定義出這些類型;而後者則是指在運行時期間動態地創建代理類,譬如Unity、Castle、Aspect Core以及ASP.NET中都可以看到這種技術的身影,即所謂的「動態編織」技術,通過反射機制和修改IL代碼來達到動態代理的目的。通常意義上的代理模式,都是指靜態代理,下面我們一起來看代碼示例:

public class RealSubject : ISubject{ public void Request() { Console.WriteLine("我是RealSubject"); }}public class ProxySubject : ISubject{ private ISubject subject; public ProxySubject(ISubject subject) { this.subject = subject; } public void Request() { this.subject.Request(); }}

通過示例代碼,我們可以注意到,在代理對象ProxySubject中持有對ISubject介面的引用,因此它可以代理任何實現了ISubject介面的類,即真實對象。在Request()方法中我們調用了真實對象的Request()方法,實際上我們可以在代理對象中增加更多的細節,譬如在Request()方法執行前後插入指定的代碼,這就是面向切面編程(AOP)的最基本的原理。在實際應用中,主要以動態代理最為常見,Java中提供了InvocationHandler介面來實現這一介面,在.NET中則有遠程調用(Remoting Proxies)、ContextBoundObjectIL織入等多種實現方式。從整體而言,生成代理類和子類化是常見的兩種思路。相比靜態代理,動態代理機制相對複雜,不適合在這裡展開來說,感興趣的朋友可以去做進一步的了解。

裝飾器模式

裝飾器模式,同樣是一種結構型模式,其核心是為了解決由繼承引發的「類型爆炸」問題。我們知道,通過繼承增加子類就可以擴展父類的功能,可隨著業務複雜性的不斷增加,子類變得越來越多,這就會引發「類型爆炸」問題。裝飾器模式就是一種用以代替繼承的技術,即無需通過繼承增加子類就可以擴展父類的功能,同時不改變原有的結構。在《西遊記》中孫悟空和二郎神鬥法,孫悟空變成了一座廟宇,這是對原有功能的一種擴展,因為孫悟空的本質依然是只猴子,不同的是此刻具備了廟宇的功能。這就是裝飾器模式。下面,我們一起來看一個生活中的例子。

喜歡喝咖啡的朋友,看到這張圖應該感到特別親切,因為咖啡的種類的確是太多啦。在開始喝咖啡以前,我完全不知道咖啡會有這麼多的種類,而且咖啡作為一種略顯小資的飲品,其名稱更是令人目不暇接,一如街頭出現的各種女孩子喜歡的茶品飲料,有些當真是教人叫不出來名字。這是一個典型的「類型爆炸」問題,人們在吃喝上堅持不懈的追求,讓咖啡的種類越來越多,這個時候繼承反而變成了一種沉重的包袱,那麼該如何解決這個問題呢?裝飾器模式應運而生,首先來看裝飾器模式的UML圖示:

從這個圖示中可以看出,裝飾器和被裝飾者都派生自同一個抽象類Component,而不同的Decorator具備不同的功能,DecoratorA可以為被裝飾者擴展狀態,DecoratorB可以為被裝飾者擴展行為,可無論如何,被裝飾者的本質不會發生變化,它還是一個Component。回到咖啡這個問題,我們發現這些咖啡都是由濃縮咖啡、水、牛奶、奶泡等組成,所以我們可以從一杯濃縮咖啡開始,對咖啡反覆進行調配,直至搭配出我們喜歡的咖啡,這個過程就是反覆使用裝飾器進行裝飾的過程,因此我們可以寫出下面的代碼:

//飲料抽象類abstract class Drink{ public abstract Drink Mix(Drink drink);}//牛奶裝飾器class MilkDecorator : Drink{ private Drink milk; MilkDecorator(Drink milk) { this.milk = milk; } public override Drink Mix(Drink coffee) { return coffee.Mix(this.milk); }}//熱水裝飾器class WaterDecorator : Drink{ private Drink water; WaterDecorator(Drink water) { this.water = water; } public override Drink Mix(Drink coffee) { return coffee.Mix(this.water); }}//一杯濃縮咖啡var coffee = new Coffee()//咖啡里混入水coffee = new WaterDecorator(new Water()).Mix(coffee)//咖啡里混入牛奶coffee = new MilkDecorator(new Milk()).Mix(coffee)

在這裡我們演示了如何通過裝飾器模式來調配出一杯咖啡,這裡我沒有寫出具體的Coffee類。在實際場景中,我們還會遇到在咖啡里加糖或者配料來收費的問題,此時裝飾器模式就可以幫助我們解決問題,不同的裝飾器會對咖啡的價格進行修改,因此在應用完所有裝飾器以後,我們就可以計算出最終這杯咖啡的價格。由此我們可以看出,裝飾器模式強調的是擴展。什麼是擴展呢,就是在不影響原來功能的基礎上增加新的功能。

區別和聯繫

代理模式和裝飾器模式都是結構型的設計模式,兩者在實現上是非常相似的。不同的地方在於,代理模式下調用者無法直接接觸到真實對象,因此代理模式強調的是控制,即向調用者隱藏真實對象的信息,控制真實對象可以訪問的範圍;裝飾器模式下,擴展功能的職責由子類轉向裝飾器,且裝飾器與被裝飾者通常是"同源"的,即派生自同一個父類或者是實現了同一個介面,裝飾器關注的是增加被裝飾者的功能,即擴展。兩者的聯繫在於都需要持有一個"同源"對象的引用,譬如代理對象與真實對象同源,裝飾器與被裝飾者同源。從調用的層面上來講,調用者無法接觸到真實對象,它調用的始終是代理對象,對真實對象的內部細節一無所知,這是代理模式;調用者可以接觸到裝飾器和被裝飾者,並且知道裝飾器會對被裝飾者產生什麼樣的影響,通常是從一個默認的對象開始"加工",這是裝飾器模式。

裝飾器與面向切面

這篇文章寫到現在,我發覺我挖了一個非常大的坑,因為這篇文章中涉及到的概念實在太多,務求每一個概念都能講得清楚透徹,有時候就像莫名立起來的flag,時間一長連我自己都覺得荒唐。有時候感覺內容越來越難寫,道理越來越難同別人講清楚。寫作從一開始堅持到現在,就如同某些固執的喜歡一樣,大概連我都不記得最初是為了什麼吧。好了,現在來說說裝飾器與面向切面。我接觸Python裝飾器的時候,自然而然想到的是.NET中的Attribute。我在越來越多的項目中使用Attribute,譬如ORM中欄位與實體的映射規則、數據模型(Data Model)中欄位的校驗規則、RESTful API/Web API設計中的路由配置等,因為我非常不喜歡Java中近乎濫用的配置文件。

C#中的Attribute實際上是一種依附在目標(AttributeTargets)上的特殊類型,它無法通過new關鍵字進行實例化,它的實例化必須依賴所依附的目標,通過分析IL代碼我們可以知道,Attribute並非是一種修飾符而是一種特殊的類,其方括弧必須緊緊挨著所依賴的目標,構造函數以及對屬性賦值均在圓括弧內完成。相比較而言,Python中的裝飾器就顯得更為順理成章些,因為Python中的裝飾器本質就是函數,裝飾器等價於用裝飾器函數去修飾一個函數。函數修飾函數,聽起來感覺不可思議,可當你理解了函數和普通對象一樣,就不會覺得這個想法不可思議。有時回想起人生會覺得充滿玄學的意味,大概是因為我們還沒有學會把自己看得普通。

通過這篇文章的梳理,我們會發現一個奇妙的現象,Java的Spring框架採用了動態代理了實現AOP,而Python的裝飾器簡直就是天生的AOP利器,從原理上來講,這兩門語言會選擇什麼樣的方案都說得通。Java是典型的面向對象編程的語言,所以不存在任何遊離於Class以外的函數,代理模式對類型的要求更為強烈些,因為我必須限制或者說要求Proxy實現裡面的方法,而裝飾器模式相對更為寬鬆些,遇到Python這樣的動態類型語言,自然會顯得事半功倍。這說明一個道理,通往山頂的道路會有無數條,從中找出最為優雅的一條,是數學家畢生的心愿。AOP是一種思想,和語言無關,我常常聽到Java的同學們宣稱AOP和IOC在Java社區里如何流行,其實這些東西本來就是可以使用不同的方式去實現的,有些重要的東西,需要你剝離開偏見去認知。

關於C#中的Attribute和AOP如何去集成,在Unity和Aspect Core這兩個框架中都有涉及,主流的AOP都在努力向這個方向去靠攏,Java中的註解同樣不會跳出這個設定,因為編程技術到了今天,語言間的差別微乎其微,我至今依然可以聽到,換一種語言就能讓問題得到解決的聲音,我想說,軟體工程是沒有銀彈的,人類社會的複雜性會永遠持續地存在下去,你看微信這樣一個社交軟體,其對朋友圈許可權的粒度之細足以令人嘆服。有朋友嘗試在C#中借鑒Python的裝飾器,並在一組文章中記錄了其中的心得,這裡分享給大家,希望對這個問題有興趣的朋友,可以繼續努力研究下去,AOP採用哪種方式實現重要嗎?有人用它做許可權控制,有人用它做日誌記錄......允許差異的存在,或許才是我們真正需要從這個世界裡汲取的力。

輕量級AOP框架-移植python的裝飾器(Decorator)到C#(思考篇)

輕量級AOP框架-移植python的裝飾器(Decorator)到C#(編碼篇)

本文小結

本文是博主學習Python時臨時起意的想法,因為曾經有在項目中使用過AOP的經驗,所以在學習Python中的裝飾器的時候,自然而然地對這個特性產生了興趣。有人說,裝飾器是Python進階的重要知識點。在今天這篇文章中,我們首先從Python中的函數引出"函數對象"這一概念,在闡述這個概念的過程中,穿插了函數式編程、高階函數、lambda等等的概念,"函數是一等公民",這句話在Python中出現時就是指裝飾器,因為裝飾器的本質就是函數。然後我們討論了兩種和裝飾器有關的設計模式,即代理模式和裝飾器模式,選擇這兩種模式來討論,是因為我們在Java/C#和Python中看到了兩種截然不同的實現AOP的思路,這部分需要花功夫去精心雕琢。博主有時候覺得力不從心,所以寫作中有不周到的地方希望大家諒解,同時積極歡迎大家留言,這篇文章就先寫到這裡吧,謝謝大家!


推薦閱讀:

面向新手的雜談:Flyweight(續)

TAG:Python | 裝飾器 | 設計模式 |