裝飾器(decorator)是一種高級Python語法。可以對一個函數(shù)、方法或者類進(jìn)行加工。在Python中,我們有多種方法對函數(shù)和類進(jìn)行加工,相對于其它方式,裝飾器語法簡單,代碼可讀性高。因此,裝飾器在Python項目中有廣泛的應(yīng)用。修飾器經(jīng)常被用于有切面需求的場景,較為經(jīng)典的有插入日志、性能測試、事務(wù)處理, Web權(quán)限校驗, Cache等。很有名的例子,就是咖啡,加糖的咖啡,加牛奶的咖啡。本質(zhì)上,還是咖啡,只是在原有的東西上,做了“裝飾”,使之附加一些功能或特性。 裝飾器的優(yōu)點是能夠抽離出大量函數(shù)中與函數(shù)功能本身無關(guān)的雷同代碼并繼續(xù)重用。即,可以將函數(shù)“修飾”為完全不同的行為,可以有效的將業(yè)務(wù)邏輯正交分解。概括的講,裝飾器的作用就是為已經(jīng)存在的對象添加額外的功能。例如記錄日志,需要對某些函數(shù)進(jìn)行記錄。笨的辦法,每個函數(shù)加入代碼,如果代碼變了,就悲催了。裝飾器的辦法,定義一個專門日志記錄的裝飾器,對需要的函數(shù)進(jìn)行裝飾。 Python 的 Decorator在使用上和Java/C#的Annotation很相似,都是在方法名前面加一個@XXX注解來為這個方法裝飾一些東西。但是,Java/C#的Annotation也很讓人望而卻步,在使用它之前你需要了解一堆Annotation的類庫文檔,讓人感覺就是在學(xué)另外一門語言。而Python使用了一種相對于Decorator Pattern和Annotation來說非常優(yōu)雅的方法,這種方法不需要你去掌握什么復(fù)雜的OO模型或是Annotation的各種類庫規(guī)定,完全就是語言層面的玩法:一種函數(shù)式編程的技巧。 在Python中,裝飾器實現(xiàn)是十分方便。原因是:函數(shù)可以被扔來扔去。 python的函數(shù)就是對象 要理解裝飾器,就必須先知道,在python里,函數(shù)也是對象(functions are objects)。明白這一點非常重要,讓我們通過一個例子來看看為什么。 def shout(word='yes'): return word.capitalize()+'!' python 函數(shù)的另一個有趣的特性是,它們可以在另一個函數(shù)體內(nèi)定義。 def talk(): # 你可以在 'talk' 里動態(tài)的(on the fly)定義一個函數(shù)... def whisper(word='yes'): return word.lower()+'...' # ... 然后馬上調(diào)用它! print whisper() # 每當(dāng)調(diào)用'talk',都會定義一次'whisper',然后'whisper'在'talk'里被調(diào)用 函數(shù)引用(Functions references) 你剛剛已經(jīng)知道了,python的函數(shù)也是對象,因此:
那么,這樣就意味著一個函數(shù)可以返回另一個函數(shù) :-),來看個例子: def getTalk(type='shout'): # 我們先動態(tài)定義一些函數(shù) def shout(word='yes'): return word.capitalize()+'!' def whisper(word='yes') : return word.lower()+'...'; # 然后返回其中一個 if type == 'shout': # 注意:我們是在返回函數(shù)對象,而不是調(diào)用函數(shù), # 所以不要用到括號 '()' return shout else: return whisper # 那你改如何使用這個怪獸呢?(How do you use this strange beast?) # 先把函數(shù)賦值給一個變量 既然可以返回一個函數(shù),那么也就可以像參數(shù)一樣傳遞: def doSomethingBefore(func): print 'I do something before then I call the function you gave me' print func() doSomethingBefore(scream) #outputs: 現(xiàn)在已經(jīng)具備了理解裝飾器的所有基礎(chǔ)知識了。裝飾器也就是一種包裝材料,它們可以讓你在執(zhí)行被裝飾的函數(shù)之前或之后執(zhí)行其他代碼,而且不需要修改函數(shù)本身。 手工制作裝飾器(Handcrafted decorators) 你可以像這樣來定制: # 一個裝飾器是一個需要另一個函數(shù)作為參數(shù)的函數(shù) 現(xiàn)在你大概希望,每次調(diào)用 a_stand_alone_function 時,實際調(diào)用的是a_stand_alone_function_decorated 。這很容易,只要把 my_shiny_new_decorator 返回的函數(shù)覆蓋 a_stand_alone_function 就可以了: a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function) a_stand_alone_function() #outputs: 揭秘裝飾器(Decorators demystified) 我們用裝飾器的語法來重寫一下前面的例子: @my_shiny_new_decorator 是的,這就完了,就這么簡單。@decorator 只是下面這條語句的簡寫(shortcut): another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)裝飾器其實就是裝飾器模式的一個python化的變體(pythonic variant)。為了方便開發(fā),python已經(jīng)內(nèi)置了好幾種經(jīng)典的設(shè)計模式,比如迭代器(iterators)。 當(dāng)然,你還可以堆積使用裝飾器(you can cumulate decorators): def bread(func): def wrapper(): print '' func() print '<\______>\______>' return wrapper def ingredients(func): def wrapper(): print '#tomatoes#' func() print '~salad~' return wrapper def sandwich(food='--ham--'): print food sandwich() #outputs: --ham-- sandwich = bread(ingredients(sandwich)) sandwich() #outputs: 用python的裝飾器語法表示: 裝飾器放置的順序 很重要: 給裝飾器函數(shù)傳參(Passing arguments to the decorated function) 含參的裝飾器 在上面的裝飾器調(diào)用中,比如@decorator,該裝飾器默認(rèn)它后面的函數(shù)是唯一的參數(shù)。裝飾器的語法允許我們調(diào)用decorator時,提供其它參數(shù),比如@decorator(a)。這樣,就為裝飾器的編寫和使用提供了更大的靈活性。 上面的pre_str是允許參數(shù)的裝飾器。它實際上是對原有裝飾器的一個函數(shù)封裝,并返回一個裝飾器。我們可以將它理解為一個含有環(huán)境參量的閉包。當(dāng)我們使用@pre_str(‘^_^’)調(diào)用的時候,Python能夠發(fā)現(xiàn)這一層的封裝,并把參數(shù)傳遞到裝飾器的環(huán)境中。該調(diào)用相當(dāng)于: 裝飾方法(Decorating methods) Python的一個偉大之處在于:方法和函數(shù)幾乎是一樣的(methods and functions are really the same),除了方法的第一個參數(shù)應(yīng)該是當(dāng)前對象的引用(也就是 self)。這也就意味著只要記住把 self 考慮在內(nèi),你就可以用同樣的方法給方法創(chuàng)建裝飾器了: 當(dāng)然,如果你想編寫一個非常通用的裝飾器,可以用來裝飾任意函數(shù)和方法,你就可以無視具體參數(shù)了,直接使用 *args, **kwargs 就行: 裝飾類 在上面的例子中,裝飾器接收一個函數(shù),并返回一個函數(shù),從而起到加工函數(shù)的效果。在Python 2.6以后,裝飾器被拓展到類。一個裝飾器可以接收一個類,并返回一個類,從而起到加工類的效果。 在decorator中,我們返回了一個新類newClass。在新類中,我們記錄了原來類生成的對象(self.wrapped),并附加了新的屬性total_display,用于記錄調(diào)用display的次數(shù)。我們也同時更改了display方法。通過修改,我們的Bird類可以顯示調(diào)用display的次數(shù)了。 給裝飾器傳參(Passing arguments to the decorator) 現(xiàn)在對于給裝飾器本身傳參數(shù),你有什么看法呢?好吧,這樣說有點繞,因為裝飾器必須接受一個函數(shù)作為參數(shù),所以就不能把被裝飾的函數(shù)的參數(shù),直接傳給裝飾器(you cannot pass the decorated function arguments directly to the decorator.) 在直奔答案之前,我們先寫一個小提示: 這完全一樣,都是 my_decorator 被調(diào)用。所以當(dāng)你使用 @my_decorator 時,你在告訴 python 去調(diào)用 “被變量 my_decorator 標(biāo)記的” 函數(shù)(the function ‘labeled by the variable “my_decorator”‘)。這很重要,因為你給的這個標(biāo)簽?zāi)苤苯又赶蜓b飾器或者其他!
不要感到驚訝,讓我們做一件完全一樣的事情,只不過跳過了中間變量: def decorated_function(): print 'I am the decorated function.' 再做一次,代碼甚至更短: @decorator_maker() 我們在用 @ 語法調(diào)用了函數(shù) , 那么回到帶參數(shù)的裝飾器。如果我們能夠使用一個函數(shù)動態(tài)(on the fly)的生成裝飾器,那么我們就能把參數(shù)傳遞給那個函數(shù),對嗎? def decorator_maker_with_arguments(decorator_arg1, decorator_arg2): print 'I make decorators! And I accept arguments:', decorator_arg1, decorator_arg2 def my_decorator(func): # 在這里能傳參數(shù)是一個來自閉包的饋贈. # 如果你對閉包感到不舒服,你可以直接忽略(you can assume it's ok), # 或者看看這里: http:///questions/13857/can-you-explain-closures-as-they-relate-to-python print 'I am the decorator. Somehow you passed me arguments:', decorator_arg1, decorator_arg2 # 不要把裝飾器參數(shù)和函數(shù)參數(shù)搞混了! def wrapped(function_arg1, function_arg2) : print ('I am the wrapper around the decorated function.\n' 'I can access all the variables\n' '\t- from the decorator: {0} {1}\n' '\t- from the function call: {2} {3}\n' 'Then I can pass them to the decorated function' .format(decorator_arg1, decorator_arg2, function_arg1, function_arg2)) return func(function_arg1, function_arg2) return wrapped return my_decorator @decorator_maker_with_arguments('Leonard', 'Sheldon') def decorated_function_with_arguments(function_arg1, function_arg2): print ('I am the decorated function and only knows about my arguments: {0}' ' {1}'.format(function_arg1, function_arg2)) decorated_function_with_arguments('Rajesh', 'Howard') #outputs: 這就是了,帶參數(shù)的裝飾器。參數(shù)也可以設(shè)置為變量:
如你所見,你可以給裝飾器傳遞參數(shù),就好像其他任意一個使用了這種把戲的函數(shù)一樣(。如果你愿意,甚至可以使用 *args, **kwargs。但是,記住,裝置器只調(diào)用一次,僅當(dāng)python導(dǎo)入這個腳本時。你不能在之后動態(tài)的設(shè)置參數(shù)。當(dāng)你執(zhí)行 import x 時,這個函數(shù)已經(jīng)被裝飾了,因此你不能修改任何東西。 實踐:裝飾器裝飾一個裝飾器(Let’s practice: a decorator to decorate a decorator) 我將展示一段能用來創(chuàng)建能接受通用的任意參數(shù)的裝飾器的代碼。畢竟,為了能接受參數(shù),我們用了另一個函數(shù)來創(chuàng)建我們的裝飾器。我們包裝了裝飾器。在我們剛剛看到的東西里,還有用來包裝函數(shù)的嗎?是的,就是裝飾器。讓我們給裝飾器寫一個裝飾器來玩玩: def decorator_with_args(decorator_to_enhance): ''' This function is supposed to be used as a decorator. It must decorate an other function, that is intended to be used as a decorator. Take a cup of coffee. It will allow any decorator to accept an arbitrary number of arguments, saving you the headache to remember how to do that every time. ''' # We use the same trick we did to pass arguments def decorator_maker(*args, **kwargs): # We create on the fly a decorator that accepts only a function # but keeps the passed arguments from the maker. def decorator_wrapper(func): # We return the result of the original decorator, which, after all, # IS JUST AN ORDINARY FUNCTION (which returns a function). # Only pitfall: the decorator must have this specific signature or it won't work: return decorator_to_enhance(func, *args, **kwargs) return decorator_wrapper return decorator_maker它可以像這樣使用: # You create the function you will use as a decorator. And stick a decorator on it :-) 我知道,你上一次有這種感覺,是在聽一個人說“在理解遞歸之前,你必須先理解遞歸”之后。但是現(xiàn)在,掌握之后,你不覺得很爽嗎? 裝飾器最佳實踐(Best practices while using decorators) 裝飾器是在 python 2.4 之后才有的,所以先確定你的代碼運行時; 記住這點:裝飾器降低了函數(shù)調(diào)用效率; 你不能“解裝飾”一個函數(shù)。有一些能用來創(chuàng)建可以移除的裝飾器的方法,但沒人用它們。所以一個函數(shù)一旦被裝飾了,就結(jié)束了(不能改變了)。 裝飾器包裝了函數(shù),這使得會難以調(diào)試。 Python 2.5 通過提供了一個 functools 模塊解決了最后一個問題。functools.wraps 把任意被包裝函數(shù)的函數(shù)名、模塊名和 docstring 拷貝給了 wrapper. 有趣的事是,functools.wraps 也是一個裝飾器:-)
裝飾器如何才能有用(How can the decorators be useful?) 現(xiàn)在問題來了:我能用裝飾器來干嘛?看起來很酷也很強大,但是來一個實際例子才更好。一個典型的用途是,用來擴(kuò)展一個外部導(dǎo)入的函數(shù)(你不能修改)的行為,或者為了調(diào)試(你不想修改這個函數(shù),因為只是暫時的)。你也可以用裝飾器實現(xiàn)只用一段相同的代碼來擴(kuò)展成幾個不同的函數(shù),而且你不需要每次都重寫這段代碼。這樣就是常說的 DRY。比如: def benchmark(func): ''' A decorator that prints the time a function takes to execute. ''' import time def wrapper(*args, **kwargs): t = time.clock() res = func(*args, **kwargs) print func.__name__, time.clock()-t return res return wrapper def logging(func): ''' A decorator that logs the activity of the script. (it actually just prints it, but it could be logging!) ''' def wrapper(*args, **kwargs): res = func(*args, **kwargs) print func.__name__, args, kwargs return res return wrapper def counter(func): ''' A decorator that counts and prints the number of times a function has been executed ''' def wrapper(*args, **kwargs): wrapper.count = wrapper.count + 1 res = func(*args, **kwargs) print '{0} has been used: {1}x'.format(func.__name__, wrapper.count) return res wrapper.count = 0 return wrapper @counter 當(dāng)然,裝飾器的好處就是你可以幾乎用來裝飾所有東西,而且不要重寫。也就是我說的 DRY:
Python 語言本身也提供了一些裝飾器:property、staticmethod 等。Django 用裝飾器來管理換成和視圖權(quán)限。Twisted 用來偽裝 內(nèi)聯(lián)異步函數(shù)調(diào)用。 總結(jié) 裝飾器的核心作用是name binding。這種語法是Python多編程范式的又一個體現(xiàn)。大部分Python用戶都不怎么需要定義裝飾器,但有可能會使用裝飾器。鑒于裝飾器在Python項目中的廣泛使用,了解這一語法是非常有益的。 ●本文編號79,以后想閱讀這篇文章直接輸入79即可。 ●輸入m可以獲取到文章目錄 推薦《15個技術(shù)類公眾微信》 涵蓋:程序人生、算法與數(shù)據(jù)結(jié)構(gòu)、黑客技術(shù)與網(wǎng)絡(luò)安全、大數(shù)據(jù)技術(shù)、前端開發(fā)、Java、Python、Web開發(fā)、安卓開發(fā)、iOS開發(fā)、C/C++、.NET、Linux、數(shù)據(jù)庫、運維等。 |
|
|