翻譯前想說的話:
這是一篇介紹python裝飾器的文章,對比之前看到的類似介紹裝飾器的文章,個人認為無人可出其右,文章由淺到深,由函數介紹到裝飾器的高級應用,每個介紹必有例子說明。文章太長,看完原文后我計劃按照文章作者的劃分,將分為兩章翻出來和大家分享,如果你覺得干的還不錯,就點個贊吧.
目錄:
函數
一等對象
內部函數
從函數中返回函數
簡單裝飾器
語法糖
復用裝飾器
裝飾器傳參
從裝飾器返回值
你是誰?
一些現實中的例子
時間函數
調試代碼
給代碼降速
注冊插件
用戶是否登錄?
有想象力的裝飾器
裝飾類
嵌套的裝飾器
帶參數的裝飾器
Both
Please, But Never Mind the Bread
這句話開始我不知道怎么翻,直到我看到了維尼熊......,請在這里www.google.com檢索Winnie the Pooh? Both
Please, But Never Mind the Bread
有狀態的裝飾器
類裝飾器
更多現實中的例子
代碼降速,重新訪問
創建單例模式
緩存返回值
添加單元信息
驗證JSON
正文開始:
在本次的裝飾器教程中,將介紹何為裝飾器以及如何創建和使用它們,裝飾器提供了簡單的語法來調用高階函數。
從定義上講,裝飾器是一個函數,它接收另一個函數作為參數并且擴展它的功能,但不會顯式的去修改它
說起來可能會讓人覺得難理解,但它(裝飾器)確實不會這么做,特別是一會你會看到一些裝飾器如何工作的例子
函數
在理解裝飾器之前,你首先需要理解函數如何工作。函數會基于給定的參數返回值。這里有一個非常簡單的例子:
>>> defadd_one(number):
...return number + 1
>>> add_one(2)3
通常情況下,函數在python中也會有其它功效而不是僅僅接收輸入并返回輸出。print()函數是一個例子。在控制臺輸出的時候它會返回None(1),然而,為了理解裝飾器,
將函數認為是接收參數并返回值就足夠了
注意:在面向函數編程,你幾乎只會使用純函數,不會有其它功能,然而python不是一個純函數式語言,python支持許多函數式編程概念,包括一等對象
一等對象
在python中,函數是一等對象,意思是函數可以作為參數被傳遞,就像其它的對象(string,int,fload,list和其它),思考下面的三個函數
defsay_hello(name):return f"Hello {name}"
defbe_awesome(name):return f"Yo {name}, together we are the awesomest!"
defgreet_bob(greeter_func):return greeter_func("Bob")
在這里,say_hello()和be_awsone()是常規函數,接收一個name參數返回一個字符串,然而greet_bob()函數,接收一個函數作為他的參數,我們可以將say_hello()或者be_awesome()函數傳遞給它
>>>greet_bob(say_hello)'Hello Bob'
>>>greet_bob(be_awesome)'Yo Bob, together we are the awesomest!'
注意greet_bob(say_hello) 涉及到兩個函數,但是不同的是:greet_bob()和say_hello,say_hello函數并沒有使用(),代表只傳遞了對函數的引用,函數沒有運行,greet_bob()函數,是使用了括號,所以它會被正常調用
內部函數
在函數內定義函數是被允許的。這類函數被稱為內部函數,這里有一個函數和兩個內函數的例子
defparent():print("Printing from the parent() function")deffirst_child():print("Printing from the first_child() function")defsecond_child():print("Printing from the second_child() function")
second_child()
first_child()
當你調用parent()的時候會發生什么? 請考慮一分鐘。會出現下面的輸出結果
>>>parent()
Printingfromthe parent() function
Printingfromthe second_child() function
Printingfrom the first_child() function
注意內部函數定義的順序無關緊要,和其它的函數一樣,打印只會發生在內部函數運行的時候
而且,內部函數在父函數被調用之前不會生效,它們的局部作用域是父(),它們只作為局部變量存在在父()函數的內部,嘗試調用first_child(),你會得到下面的錯誤
Traceback (most recent call last):
File"", line 1, in NameError: name'first_child' is not defined
不管你何時調用parent(),內部函數first_child()和second_child()都會被調用,因為它們的局部作用域,它們無法再parent()函數外使用
從函數中返回函數
python允許使用函數來作為返回值,下面的例子從外部的父函數parent()返回了一個內部函數
defparent(num):deffirst_child():return "Hi, I am Emma"
defsecond_child():return "Call me Liam"
if num == 1:returnfirst_childelse:return second_chil
注意這里返回的first_child是沒有括號的,也就是返回了對函數first_child的引用, 帶括號的first_child() 指的是對函數求值的結果,這個可以在下面的實例中看到
>>> first = parent(1)>>> second = parent(2)>>>first.first_child at 0x7f599f1e2e18>
>>>second.second_child at 0x7f599dad5268>
這個輸出代表first變量引用了在parent()中的本地函數first_child(),second則指向了second_child()
你現在可以像常規函數一樣使用first和second,雖然他們指向的函數無法被直接訪問
>>>first()'Hi, I am Emma'
>>>second()'Call me Liam'
請注意,在前面的例子中我們在父函數中運行內部函數,例如first_child(),然后在最后的例子中,返回的時候沒有給內部函數first_child添加括號。這樣,就獲取了將來可以調用的函數的引用。這樣有意義嗎?
簡單裝飾器
現在你已經看到函數和python中的其它對象一樣,你已經準備好前進來認識python裝飾器,讓我們以一個例子開始:
defmy_decorator(func):defwrapper():print("Something is happening before the function is called.")
func()print("Something is happening after the function is called.")returnwrapperdefsay_whee():print("Whee!")
say_whee= my_decorator(say_whee)
你能猜到當你調用say_whee()的時候回發生什么么?試一下:
>>>say_whee()
Somethingis happening before the function iscalled.
Whee!
Somethingis happening after the function is called.
要理解這里發生了什么,需要回看下之前的例子,我們只是應用了你到目前為止學到的所有東西
所謂的裝飾器發生在下面這行:
say_whee = my_decorator(say_whee)
事實上,say_whee現在指向了內部函數wrapper(),當你調用my_decorator(say_whee)的時候會將wrapper作為函數返回
>>>say_whee.wrapper at 0x7f3c5dfd42f0>
wrapper()引用原始的say_whee()作為func,在兩個print()之間調用這個函數
簡而言之:裝飾器包裹一個函數,并改變它的行為
在繼續之前,讓我們看下第二個例子。因為wrapper()是一個常規的函數,裝飾器可以以一種動態的方式來修改函數。為了不打擾你的鄰居,下面的示例演示只會在白天運行的裝飾器
from datetime importdatetimedefnot_during_the_night(func):defwrapper():if 7 <= datetime.now().hour < 22:
func()else:pass #Hush, the neighbors are asleep
returnwrapperdefsay_whee():print("Whee!")
say_whee= not_during_the_night(say_whee)
如果你在睡覺的時間調用say_whee(),不會發生任何事情
>>>say_whee()>>>
語法糖
上面的裝飾器say_whee()用起來有一點笨拙。首先,你鍵入了三次say_whee,另外,裝飾器隱藏在了函數的定義之下
作為替代,python允許你使用@symbol的方式使用裝飾器,有時被稱為"pie"語法,下面的例子和之前第一個裝飾器做了同樣的事情
defmy_decorator(func):defwrapper():print("Something is happening before the function is called.")
func()print("Something is happening after the function is called.")returnwrapper
@my_decoratordefsay_whee():print("Whee!")
所以,@my_decorator 只是say_whee = my_decorator(say_whee)的一種快捷方式,這就是如何將裝飾器應用到函數上
復用裝飾器
回想一下,裝飾器只是一個普通的函數。所有常用的工具都是方便重復利用的,讓我們將裝飾器移動到他自己的模型上以便于在其它的函數上使用
下面創建了一個decorators.py
defdo_twice(func):defwrapper_do_twice():
func()
func()return wrapper_do_twice
注意:你可以隨意定義內部函數的名稱,通常像wrapper()用起來是沒問題的。你在這篇文章中會遇到許多裝飾器。為了區別開它們,我們將使用decorator名稱來命名內部函數,但會加上wrapper_前綴。
你可以使用常規導入來使用一個新的裝飾器
from decorators importdo_twice
@do_twicedefsay_whee():print("Whee!")
當你運行這個例子,你會看到原始韓式say_whee()執行兩次
>>>say_whee()
Whee!
Whee!
裝飾器傳參
如果你有一個函數需要接收一些參數,這時候還可以再使用裝飾器么,然我們試試
from decorators importdo_twice
@do_twicedefgreet(name):print(f"Hello {name}")
不幸的是,運行代碼拋出了錯誤
>>> greet("World")
Traceback (most recent call last):
File"", line 1, in TypeError: wrapper_do_twice() takes 0 positional arguments but1 was given
問題在于內部函數wrapper_do_twice()沒有接收任何參數,但是name="World"卻傳給了它。你可以讓wrapper_do_twice()接收一個參數來修補這個問題,但是這樣前面的say_whee()函數就無法工作了
解決方案是在內部函數使用*args和**kwargs ,這樣它會允許接收任意個關鍵參數,下面重寫了decorators.py
defdo_twice(func):def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)return wrapper_do_twice
內部函數wrapper_do_twice()現在接收任意數量的參數并會傳遞給裝飾的函數,目前say_whee()和greet()都會正常工作
>>>say_whee()
Whee!
Whee!>>> greet("World")
Hello World
Hello World
從裝飾器返回值
被裝飾的函數返回值會發生什么?這會由裝飾器來決定,我們下面有一個簡單的裝飾器函數
from decorators importdo_twice
@do_twicedefreturn_greeting(name):print("Creating greeting")return f"Hi {name}"
嘗試運行它:
>>> hi_adam = return_greeting("Adam")
Creating greeting
Creating greeting>>> print(hi_adam)
None
裝飾器吃掉了從函數返回的值
因為do_twice_wrapper()沒有返回值,調用 return_greeting("Adam") 最后返回了None
修復的方式是,需要確認裝飾器返回它裝飾的函數的值,改變decorators.py文件:
defdo_twice(func):def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)return func(*args, **kwargs)return wrapper_do_twice
執行這個函數返回的值:
>>> return_greeting("Adam")
Creating greeting
Creating greeting'Hi Adam'
你是誰?
在使用Python(尤其是在交互式shell中)時,強大的內省是非常方便的功能。內省是對象在運行時了解其自身屬性的能力。例如,函數知道自己的名稱和文檔:
>>> print.__name__
'print'
>>> help(print)
Help on built-in function print inmodule builtins:print(...)
內省同樣適用于你自定義的函數:
>>>say_whee.wrapper_do_twice at 0x7f43700e52f0>
>>> say_whee.__name__
'wrapper_do_twice'
>>>help(say_whee)
Help on function wrapper_do_twiceinmodule decorators:
wrapper_do_twice()
然而在被裝飾后,say_whee()會對自身感到疑惑。它現在顯示為 do_twice()裝飾器的內部函數 wrapper_do_twice()
為了修復這個,裝飾器需要使用@functools.wraps裝飾器,它會保留原始函數的信息,再次更新下decorators.py:
importfunctoolsdefdo_twice(func):
@functools.wraps(func)def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)return func(*args, **kwargs)return wrapper_do_twice
不需要對被裝飾的say_whee()函數做任何更改:
>>>say_whee
>>> say_whee.__name__
'say_whee'
>>>help(say_whee)
Help on function say_wheeinmodule whee:
say_whee()
非常好,現在say_whee()在被裝飾后可以保持自己
技術細節:@funtools.wraps 裝飾器使用函數functools.update_wrapper()來更新指定的屬性,像__name__和__doc__來用于自省
一些現實中的例子
讓我們看一些用處更大的裝飾器例子。你會注意到他們主要的模式和你現在所學的都是一樣的
importfunctoolsdefdecorator(func):
@functools.wraps(func)def wrapper_decorator(*args, **kwargs):#Do something before
value = func(*args, **kwargs)#Do something after
returnvaluereturn wrapper_decorator
對于構建更復雜的裝飾器,這個是一個很好的模板
時間函數
讓我們從@timer裝飾器開始,它會測量函數運行的時間并且打印持續時間到控制臺,這是代碼:
importfunctoolsimporttimedeftimer(func):"""Print the runtime of the decorated function"""@functools.wraps(func)def wrapper_timer(*args, **kwargs):
start_time= time.perf_counter() #1
value = func(*args, **kwargs)
end_time= time.perf_counter() #2
run_time = end_time - start_time #3
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")returnvaluereturnwrapper_timer
@timerdefwaste_some_time(num_times):for _ inrange(num_times):
sum([i**2 for i in range(10000)])
這個函數是在函數運行之前獲取時間(#1行),并且在函數運行結束之后獲取時間(#2行),我們使用 time.perf_counter() 函數,這個函數可以非常好的計算時間間隔。下面是一個示例:
>>> waste_some_time(1)
Finished'waste_some_time' in 0.0010secs>>> waste_some_time(999)
Finished'waste_some_time' in 0.3260 secs
自己運行測試下,手敲下這里的代碼,確保你理解它的工作原理。如果不明白,也不要擔心。裝飾器是高級方法,試著思考下或者畫下流程圖
注意: 如果你只是想獲取函數的運行時間,@timer 裝飾器可以滿足。如果你想獲取到更精確的數據,你應該考慮使用timeit 模塊來替代它。它臨時禁用了垃圾收集并且運行多次以避免函數快速調用帶來的噪音數據
調試代碼
下面的@debug函數會在每次調用的時候打印函數被調用的參數和它的返回結果
importfunctoolsdefdebug(func):"""Print the function signature and return value"""@functools.wraps(func)def wrapper_debug(*args, **kwargs):
args_repr= [repr(a) for a in args] #1
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] #2
signature = ",".join(args_repr + kwargs_repr) #3
print(f"Calling {func.__name__}({signature})")
value= func(*args, **kwargs)print(f"{func.__name__!r} returned {value!r}") #4
returnvaluereturn wrapper_debug
signature 變量是通過 字符串表示方法 來創建所有的輸入參數。下面的數字對應了代碼中的注釋
1、將args創建為列表,使用repr修飾
2、將kwargs創建為列表,使用f-string格式化參數為key=value,!r表示使用repr()表示值
3、args和kwargs轉換后會合并在signature變量中,使用逗號分隔每個變量
4、函數運行結束后會返回值
讓我們在一個簡單的函數中使用裝飾器被觀察它是如何運行的,被裝飾的函數只有一個位置參數和一個關鍵字參數
@debugdef make_greeting(name, age=None):if age isNone:return f"Howdy {name}!"
else:return f"Whoa {name}! {age} already, you are growing up!"
注意@debug裝飾器如何打印make_greeting()函數的signature 和返回值
>>> make_greeting("Benjamin")
Calling make_greeting('Benjamin')'make_greeting' returned 'Howdy Benjamin!'
'Howdy Benjamin!'
>>> make_greeting("Richard", age=112)
Calling make_greeting('Richard', age=112)'make_greeting' returned 'Whoa Richard! 112 already, you are growing up!'
'Whoa Richard! 112 already, you are growing up!'
>>> make_greeting(name="Dorrisile", age=116)
Calling make_greeting(name='Dorrisile', age=116)'make_greeting' returned 'Whoa Dorrisile! 116 already, you are growing up!'
'Whoa Dorrisile! 116 already, you are growing up!'
@debug修飾符看起來只是重復了我們剛才寫的內容 ,并不是非常有用。 但當應用到不能直接修改的其它函數時,它會更加強大。
下面的例子計算了一個數學常數E的近似值
importmathfrom decorators importdebug#Apply a decorator to a standard library function
math.factorial =debug(math.factorial)def approximate_e(terms=18):return sum(1 / math.factorial(n) for n in range(terms))
這個例子還演示了如何將裝飾器應用到已經定義了的函數
當調用approximate_e()函數,你可以看到@debug函數在工作:
>>> approximate_e(5)
Calling factorial(0)'factorial' returned 1Calling factorial(1)'factorial' returned 1Calling factorial(2)'factorial' returned 2Calling factorial(3)'factorial' returned 6Calling factorial(4)'factorial' returned 24
2.708333333333333
在這個例子中,可以得到一個真實值的近似值e = 2.718281828
給代碼降速
下面的例子看起來可能不是很有用。可能最常見的用例是,您希望對一個不斷檢查資源是否存在的函數進行速率限制 。 @slow_down decorator在調用被修飾的函數之前會暫停一秒鐘
importfunctoolsimporttimedefslow_down(func):"""Sleep 1 second before calling the function"""@functools.wraps(func)def wrapper_slow_down(*args, **kwargs):
time.sleep(1)return func(*args, **kwargs)returnwrapper_slow_down
@slow_downdefcountdown(from_number):if from_number < 1:print("Liftoff!")else:print(from_number)
countdown(from_number- 1)
來看下@slow_down裝飾器的效果,你需要自己運行跑下
>>> countdown(3)3
2
1Liftoff!
countdown()是一個遞歸函數。也就是說,它是一個調用自身的函數 。
注冊插件
裝飾器不是必須要修飾被裝飾的函數(這句話不太好翻譯,看下面的例子理解起來很容易),它還可以簡單地注冊一個函數,并將其解包返回,例如,可以使用它來創建一個輕量級插件體系結構:
importrandom
PLUGINS=dict()defregister(func):"""Register a function as a plug-in"""PLUGINS[func.__name__] =funcreturnfunc
@registerdefsay_hello(name):return f"Hello {name}"@registerdefbe_awesome(name):return f"Yo {name}, together we are the awesomest!"
defrandomly_greet(name):
greeter, greeter_func=random.choice(list(PLUGINS.items()))print(f"Using {greeter!r}")return greeter_func(name)
@register裝飾器只是在全局PLUGINS 字典中儲存了被裝飾函數的引用。注意你不需要在例子中寫內部函數或者使用@functools.wraps ,因為返回的是一個未經過修改的初始函數
randomly_greet()函數在注冊函數中隨機選擇一個使用。注意PLUGINS字典已經包含了對注冊為插件的每個函數對象的引用:
>>>PLUGINS
{'say_hello': ,'be_awesome': }>>> randomly_greet("Alice")
Using'say_hello'
'Hello Alice'
這個插件的主要用處在于不需要再單獨維護一個插件列表。這個列表在插件注冊時自動創建,使得添加一個新插件變得很簡單,只需定義函數并用@register裝飾即可。
如果你對python中的globals()函數熟悉,你可能會看到一些和我們的插件結構相似之處。globals()可以訪問當前作用于的所有全局變量
包括我們的插件:
>>>globals()
{...,#Lots of variables not shown here.
'say_hello': ,'be_awesome': ,'randomly_greet': }
使用@register 裝飾器,可以創建感興趣的變量管理列表,有效地從globals()中篩選出一些函數
用戶是否登錄?
在繼續討論一些更有趣的裝飾器之前,讓我們在最后一個示例中演示通常在處理web框架時使用的裝飾器。在這個例子中,我們使用Flask去設置一個/secret web頁面,這個頁面只對登錄用戶或者其他有權限的用戶展示
from flask importFlask, g, request, redirect, url_forimportfunctools
app= Flask(__name__)deflogin_required(func):"""Make sure user is logged in before proceeding"""@functools.wraps(func)def wrapper_login_required(*args, **kwargs):if g.user isNone:return redirect(url_for("login", next=request.url))return func(*args, **kwargs)returnwrapper_login_required
@app.route("/secret")
@login_requireddefsecret():
...
雖然這里演示了如何對web框架添加身份驗證嗎,但通常不應該自己編寫這些類型的裝飾器。對于Flask可以使用Flask-login擴展,這里的功能更豐富也更加安全
有想象力的裝飾器
到目前為止,你已經看到了如何創建簡單的裝飾器并且非常了解什么是裝飾器以及它們是如何工作的。請從這篇文章中休息一下,練習學到的一切。
在本教程的第二部分中,我們將探索更高級的特性,包括如何使用以下特性:
1、在類上使用裝飾器(裝飾類)
2、在一個函數上應用多個裝飾器
3、帶參數的裝飾器
4、可以選擇是否接收參數的裝飾器
5、帶狀態的裝飾器
6、類裝飾器