深夜好文|裝飾器為什麼難以理解?

作者:劉志軍,6年+Python使用經驗, 高級開發工程師,目前在互聯網醫療行業從事Web系統構架工作

題圖:https://unsplash.com/@oldskool2016

無論項目中還是面試都離不開裝飾器話題,裝飾器的強大在於它能夠在不修改原有業務邏輯的情況下對代碼進行擴展,許可權校驗、用戶認證、日誌記錄、性能測試、事務處理、緩存等都是裝飾器的絕佳應用場景,它能夠最大程度地對代碼進行復用。

但為什麼初學者對裝飾器的理解如此困難,我認為本質上是對Python函數理解不到位,因為裝飾器本質上還是函數

函數的定義

理解裝飾器前,需要明白函數的工作原理,我們先從一個最簡單函數定義開始:

def foo(num): return num + 1

上面定義了一個函數,名字叫foo,也可以把foo可理解為變數名,該變數指向一個函數對象

調用函數只需要給函數名加上括弧並傳遞必要的參數(如果函數定義的時候有參數的話)

value = foo(3)print(value) # 4

變數名foo現在指向函數對象,但它也可以指向另外一個函數。

def bar(): print("bar")foo = barfoo() # bar

函數作為返回值

在Python中,一切皆為對象,函數也不例外,它可以像整數一樣作為其它函數的返回值,例如:

def foo(): return 1def bar(): return fooprint(bar()) # print(bar()()) # 1 # 等價於print(foo()) # 1

bar() 的返回值是一個函數對象,所以我們可以繼續對返回值進行調用,調用bar()()等價於調用foo(),因為 變數 foo 指向的對象與 bar() 的返回值是同一個對象。

函數作為參數

函數還可以像整數一樣作為函數的參數,例如:

def foo(num): return num + 1def bar(fun): return fun(3)value = bar(foo)print(value) # 4

函數bar接收一個參數,這個參數是一個可被調用的函數對象,把函數foo傳遞到bar中去時,foo 和 fun 兩個變數名指向的都是同一個函數對象 。所以調用 fun(3) 相當於調用 foo(3)。

函數嵌套

函數不僅可以作為參數和返回值,函數還可以定義在另一個函數中,作為嵌套函數存在,例如:

def outer(): x = 1 def inner(): print(x) inner()outer() # 1

inner做為嵌套函數,它可以訪問外部函數的變數,調用 outer 函數時,發生了3件事:

  1. 給 變數x賦值為1

  2. 定義嵌套函數inner,此時並不會執行 inner 中的代碼,因為該函數還沒被調用,直到第3步

  3. 調用 inner 函數,執行 inner 中的代碼邏輯。

閉包

再來看一個例子:

def outer(x): def inner(): print(x) return innerclosure = outer(1)closure() # 1

同樣是嵌套函數,只是稍改動一下,把局部變數 x 作為參數了傳遞進來,嵌套函數不再直接在函數里被調用,而是作為返回值返回,這裡的 closure就是一個閉包,本質上它還是函數,閉包是引用了自由變數(x)的函數(inner)。

裝飾器

繼續往下看:

def foo(): print("foo")

上面這個函數這可能是史上最簡單的業務代碼了,雖然沒什麼用,但是能說明問題就行。現在,有一個新的需求,需要在執行該函數時加上日誌:

def foo(): print("記錄日誌開始") print("foo") print("記錄日誌結束")

功能實現,唯一的問題就是它需要侵入到原來的代碼裡面,把日誌邏輯加上去,如果還有好幾十個這樣的函數要加日誌,也必須這樣做,顯然,這樣的代碼一點都不Pythonic。那麼有沒有可能在不修改業務代碼的提前下,實現日誌功能呢?答案就是裝飾器。

def outer(func): def inner(): print("記錄日誌開始") func() # 業務函數 print("記錄日誌結束") return innerdef foo(): print("foo")foo = outer(foo) foo()

我沒有修改 foo 函數裡面的任何邏輯,只是給 foo 變數重新賦值了,指向了一個新的函數對象。最後調用 foo(),不僅能列印日誌,業務邏輯也執行完了。現在來分析一下它的執行流程。

這裡的 outer 函數其實就是一個裝飾器,裝飾器是一個帶有函數作為參數並返回一個新函數的閉包,本質上裝飾器也是函數。outer 函數的返回值是 inner 函數,在 inner 函數中,除了執行日誌操作,還有業務代碼,該函數重新賦值給 foo 變數後,調用 foo() 就相當於調用 inner()

foo 重新賦值前:

重新賦值後:

另外,Python為裝飾器提供了語法糖@,它用在函數的定義處:

@outerdef foo(): print("foo")foo()

這樣就省去了手動給foo重新賦值的步驟。

到這裡不知你對裝飾器理解了沒有?當然,裝飾器還可以更加複雜,比如可以接受參數的裝飾器,基於類的裝飾器等等。


推薦閱讀:

失眠睡眠不好怎麼辦 卧室里使用藍色裝飾可助眠
裝飾素材2(png)免摳圖
看古代高大上的寶劍裝飾藝術,漲姿勢!
漂亮別緻的圍脖,裝飾你的衣領

TAG:裝飾 | 理解 | 深夜 | 裝飾器 |