python 是一門優雅的語言,有些使用方法就像魔法一樣。裝飾器(decorator)就是一種化腐朽性為神奇的技巧。最近一直都在使用 Tornado 框架,一直還是念念不忘 Flask 。Flask 是我最喜歡的 Python 框架,最早被它吸引也是源自它使用裝飾器這個語法糖(Syntactic sugar)來做 Router,讓程式碼看上去就感覺甜甜的。
Tornado 中的 Router 略顯平淡,懷念 Flask 的味道,於是很好奇的想知道 Flask 是如何使用這個魔法。通過閱讀 Flask 的原始碼,我們也可以為 Tornado 實現了一個裝飾器 Router。
當然對於剛接觸 Python 的人,也許很容易理解裝飾器本質是設計模式中的裝飾器模式。可是 Python 通過@
一個實現裝飾器的語法糖。本文的目的就是讓 @ 不再神祕。
一切都是物件
Python 裡一切都是物件,當然這不代表一切都是女朋友。函式也是物件,因而可以當成引數傳遞,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def say_english(): print 'hello' def say_chinese(): print '你好' say_english() # hello say_chinese() # 你好 def greet(say): say() greet(say_english) # hello greet(say_chinese) # 你好 |
我們的 greet 函式的引數,也是一個函式物件。可以傳遞這個引數物件。我們呼叫greet的時候,greet 內部進行函式引數的呼叫。
裝飾模式
裝飾模式,顧名思義,就是在呼叫目標函式之前,對這個函式物件進行裝飾。比如一個對資料庫操作的方法,我們在查詢資料之前,需要連線一下資料庫,當查詢結束之後,需要再把連線斷開關閉。正常的邏輯如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def connect_db(): print 'connect db' def close_db(): print 'close db' def query_user(): connect_db() print 'query the user' close_db() query_user() # connect db # query the user # close db |
我們把 連線資料庫(connect_db) 和 關閉連線 (close_db)都封裝成了函式。 query_data 方法執行我們查詢的具體邏輯。這樣需要不同的查詢方法,只需要把查詢的邏輯也封裝成一個方法就Okla
1 2 3 4 5 6 7 8 9 10 |
def query_user(): print 'query some user' def query_data(query): connect_db() query() close_db() query_data(query_user) |
把查詢的函式物件傳進來,符合開篇說的一切都是物件。裝飾器完成啦。對,就這麼簡單,query_data 就是對 query_user 的裝飾,當然你還可以寫出 query_blog 等方法。
等等,設想一種情況,在我們使用裝飾函式之前,專案的程式碼已經有了大量的 query_user方法的呼叫。如果使用了query_data 包裝。我們就不得不把之前 query_user() 的地方統統替換成 query_data(query_user)。怎麼樣才能減少對程式碼的改動呢?
我們的出發點是為了保持之前的 query_user() 不改動,現在實際情況是呼叫 query_data(query_user)。如果 query_data 呼叫的時候,返回一個函式呢?例如下面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def query_user(): print 'query some user' def query_data(query): """ 定義裝飾器,返回一個函式,對query進行wrapper包裝 """ def wrapper(): connect_db() query() close_db() return wrapper # 這裡呼叫query_data進行實際裝飾(注意裝飾是動詞) query_user = query_data(query_user) # 呼叫被裝飾後的函式query_user query_user() |
這樣一個完整的裝飾器就完成了,比起前面的版本,我們不需要改動之前寫好的 query_user 程式碼。一個關鍵點在於query_data 呼叫的時候,返回了一個 wrapper 函式,而這個wrapper 函式執行 query 函式呼叫前後的一些邏輯。另外一個關鍵就是呼叫裝飾器 query_data 裝飾函式。
語法糖@
前面的程式碼,可以使用 python的裝飾器語法糖@,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def query_data(query): def wrapper(): connect_db() query() close_db() return wrapper # 使用 @ 呼叫裝飾器進行裝飾 @query_data def query_user(): print 'query some user' query_user() |
前面的 裝飾器 呼叫進行裝飾的時候,python 有一個語法糖。
如果給裝飾器函式前面加一個@
,我們可以理解為呼叫了一些裝飾器函式,即 @query_data
等於 query_data()
。當實際上,並不是這麼使用,而是這麼一個整體:
1 2 3 |
@query_data def query_user(): print 'query some user' |
等價於
1 |
query_user = query_data(query_user) |
被裝飾函式引數
我們被裝飾的函式,往往帶有引數,因此通過裝飾器如何傳遞引數呢?回想一下,裝飾器函式針對被裝飾的函式進行裝飾,使用的是返回一個 wrapper 函式。其實這個函式可以等同於被裝飾的函式,只不過 wrapper 還做了更多的事情。被裝飾的函式引數可以通過 wrapper 傳遞。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def query_data(query): def wrapper(count): connect_db() query(count) close_db() return wrapper @query_data def query_user(count): print 'query some user limit {count}'.format(count=count) query_user(count=100) # connect db # query some user limit 100 # close db |
這樣就實現了被裝飾的函式傳遞引數。當然,位置引數和關鍵字引數,可變引數都可以。
裝飾器引數
在 flask 中,對檢視函式的裝飾是裝飾器中傳遞 url 正則,即在裝飾器中傳遞引數,和被裝飾器的引數還不一樣。
1 2 3 |
@app.router('/user') def user_page(): return 'user page' |
我們如何定義router這個裝飾器呢?其實只要在原先的裝飾器外面再包裹一層,也就是針對裝飾器進行裝飾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def router(url): print 'router invoke url', url def query_data(query): print 'query_data invoke url', url def wrapper(count): connect_db() query(count) close_db() return wrapper return query_data @router('/user') # 首先呼叫了router函式, 輸出 router invoke url /user, 進行@裝飾,輸出 'query_data invoke url', url def query_user(count): print 'query some user limit {count}'.format(count=count) query_user(count=100) # connect db # query some user limit 100 # close db |
@router() 這個語法糖看上去讓人迷惑,其實也很好理解。這裡可以看成兩個步驟
第一步是呼叫 router 這個函式:
1 |
query_data = router('/user') |
第二步則進行裝飾:
1 2 3 |
@query_data def query_user(): pass |
連起來的效果就是
1 |
query_user = router()(query_data(query_user)) |
現在回想,@
這個語法糖很甜吧。並且和python一樣很好理解,也十分常用。
當然,我們使用 裝飾器是為了實現一些需要包裝的方法,例如前面提到的 flask 的 router
有人已經寫了一篇很棒的 Tutorial: Things which aren’t magic – Flask and @app.route,可以參考加深對裝飾確定理解,裝飾器還有很多用途.
Enjoy~