我叫駱駝
會點兒代碼,會點兒讀書
這世上的書浩如煙海
我能做的就是盡量整理分享給你
起
在上一節Flask項目實戰第一彈中我們講到了路由,先看下面代碼回顧一下:
from flask import Flask
App = Flask(__name__)
@app.route("/hello")
def hello():
return "<p>Hello World ...</p>"
if __name__ == '__main__':
app.run(load_dotenv=True)
@app.route("/hello") 就是裝飾器。交流群里小伙伴問我,Python/ target=_blank class=infotextkey>Python里的裝飾器該怎么理解,今天我們好好嘮嘮這個東西。
承
說到裝飾器,我們不得不談一個知識點:閉包。我們從代碼入手,一點一點來說閉包。
Python 有一個好玩的地兒,就是 def 函數(exterior)內部可以嵌套另一個 def 函數(interior)。調用 exterior 時,若遇到 interior , 僅僅完成對于 interior 的定義,而不去運行 interior。如果 exterior return interior,那么我們可以使用 interior () 去調用 內部函數 interior 函數。
var = 0
def exterior():
var = 1
def interior():
print(var)
return interior() # 這里返回 interior 函數調用結果
exterior() # 打印 1
從上面代碼和結果中可以看到,interior 打印的 var 值 并非 第一行的 var。這說明,exterior 中的嵌套變量 var 覆蓋了全局變量var=0,然后 interior 中的本地變量按照引用規則,就引用了var = 1。
接下來,我們仔細想想下面這句話:
interior 作用域在函數結束后就立即失效,而exterior嵌套作用域在 interior 的函數返回后卻仍然有效。
var = 0
def exterior():
var = 1
def interior():
print(var)
return interior # 這里返回 interior 函數對象
inter = exterior()
inter() # 打印 1
看完上面代碼,再思考一下剛剛的話。如果還不清楚,看下圖:

圖解
創建一個閉包必須滿足以下幾點:
- 必須有一個內嵌函數
- 內嵌函數必須引用外部函數中的變量
- 外部函數的返回值必須是內嵌函數
轉
現在有了閉包的知識點,我們再聊聊裝飾器(decorator)。我要掰開了揉碎了來說說裝飾器。
剛剛接觸裝飾器的同學會對這個概念感到迷茫,然后你在網上(尤其 csdn)找例子或者教程,基本千篇一律,或者講解的“點到為止”,你在看完之后,或許更迷茫了。
函數是什么
在說裝飾器前,我們聊聊 Python 的函數。眾所周知:在 Python 中,一切皆對象,函數是一等對象。
編程語言理論家把“一等對象”定義為滿足下述條件的程序實體:
- 在運行時創建①
- 能賦值給變量或者數據結構中的元素②
- 能作為參數傳遞給函數③
- 能作為函數的返回結果④
我們看這么一段程序:
def double(x: int) -> int:
return x * 2
這段代碼很簡單,計算了一個整數的2倍。那么我么用 dis 模塊進行反編譯,看看他是怎么運行的。
>>>from my_test import double
>>>from dis import dis
>>>dis(double) # 結果如下
源碼行號 |
指令在函數中的偏移 |
指令符號 |
指令參數 |
實際參數值 |
2 |
0 |
LOAD_FAST |
0 |
x |
|
2 |
LOAD_CONST |
1 |
2 |
|
4 |
BINARY_MULTIRLY |
|
|
|
6 |
RETURN_VALUE |
|
|
指令符號解釋:
- LOAD_FAST :一般加載局部變量的值,也就是讀取值,用于計算或者函數調用傳參等;
- LOAD_CONST :加載 const 變量,比如數值、字符串等等;
- BINARY_MULTIRLY:見名知意,二進制乘法
- RETURN_VALUE:返回值
結合反編譯的結果,仔細理解一下代碼的運行流程。下面我們看另外一個例子:
def double(x: int) -> int:
return x * 2
def triple(x: int) -> int:
return x * 3
def call_func(func, x: int) -> int:
return func(x)
result = call_func(triple, 2)
print(result)
dis(call_func)
源碼行號 |
指令在函數中的偏移 |
指令符號 |
指令參數 |
實際參數值 |
10 |
0 |
LOAD_FAST |
0 |
func |
|
2 |
LOAD_FAST |
1 |
x |
|
4 |
CALL_FUNCTION |
1 |
|
|
6 |
RETURN_VALUE |
|
|
在運行過程中:出現了 CALL_FUNCTION 。結合第13行代碼,仔細體會一下這句話:函數能作為參數傳遞給另外一個函數。
我們現在看一下閉包的執行流程
def call_func():
def double(x: int) -> int:
return x * 2
return double

dis(call_func)
里邊出現了一個關鍵詞:MAKE_FUNCTION,見名知意,創建函數。此時再回想“一等對象”所滿足的條件。
說了這么多,無非是想告訴大家一個重要的東西,函數就是對象,可以被另一個函數返回,可以被賦值,也可以被調用。
其實到這里,才真是說完閉包這個東西。裝飾器和閉包大同小異,下面我們接著來。
裝飾器
有這種一種等價語法:
def callfunc(func):
return 1
@callfunc
def triple(x: int) -> int:
return x * 3
等價于
def callfunc(func):
return 1
def triple(x: int) -> int:
return x * 3
triple = callfunc(triple)
無論上面那種方式,我們輸出的 tripre 這個對象的值都是 1
>>> print(triple)
>>> 1
所以,閉包可以寫成@這種形式呢?其實,裝飾器可以理解為閉包的一種,我們可以這樣認為:閉包傳遞的是變量,而裝飾器傳遞的是函數,除此之外沒有任何區別。
我們看一個打印時間的裝飾器:
import time
def timeit(func):
def wrapper(x):
start = time.time()
ret = func(x)
print(time.time() - start)
return ret
return wrapper
@timeit
def my_func(x):
time.sleep(x)
my_func(1)
timeit 裝飾器就打印 my_func 函數的運行時間。是不是在了解完閉包之后很簡單了。
裝飾器的作用就是:在不改變原函數的情況下,對已有函數進行額外的功能擴展。
恭喜你,Python 技能又進一步。
回到 Flask 我們看看路由裝飾器
Flask 中路由的裝飾器很簡單,我們以 route 為例,以下是 route 函數源碼(抽離版):
import typing as t
def add_url_rule(rule, endpoint, f, param):
pass
def route(rule: str, **options: t.Any) -> t.Callable:
def decorator(f: t.Callable) -> t.Callable:
endpoint = options.pop("endpoint", None)
add_url_rule(rule, endpoint, f, **options)
return f
return decorator
route 函數就是一個裝飾器,內部嗲用 add_url_rule 實現真正的路由添加。再回過頭看看裝飾器的作用和定義以及使用,是不是明白了許多!加油,慢就快,快就是慢。