裝飾器為什么難以理解?
無論項目中還是面試都離不開裝飾器話題,裝飾器的強大在于它能夠在不修改原有業務邏輯的情況下對代碼進行擴展,權限校驗、用戶認證、日志記錄、性能測試、事務處理、緩存等都是裝飾器的***應用場景,它能夠***程度地對代碼進行復用。
但為什么初學者對裝飾器的理解如此困難,我認為本質上是對Python函數理解不到位,因為裝飾器本質上還是函數。
函數的定義
理解裝飾器前,需要明白函數的工作原理,我們先從一個最簡單函數定義開始:
- def foo(num):
- return num + 1
上面定義了一個函數,名字叫foo,也可以把 foo 可理解為變量名,該變量指向一個函數對象
調用函數只需要給函數名加上括號并傳遞必要的參數(如果函數定義的時候有參數的話)
- value = foo(3)
- print(value) # 4
變量名 foo 現在指向
- def bar():
- print("bar")
- foo = bar
- foo() # bar
函數作為返回值
在Python中,一切皆為對象,函數也不例外,它可以像整數一樣作為其它函數的返回值,例如:
- def foo():
- return 1
- def bar():
- return foo
- print(bar()) # <function foo at 0x10a2f4140>
- print(bar()()) # 1
- # 等價于
- print(foo()) # 1
bar() 的返回值是一個函數對象,所以我們可以繼續對返回值進行調用,調用bar()()等價于調用 foo(),因為 變量 foo 指向的對象與 bar() 的返回值是同一個對象。
函數作為參數
函數還可以像整數一樣作為函數的參數,例如:
- def foo(num):
- return num + 1
- def 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件事:
- 給 變量 x 賦值為1
- 定義嵌套函數 inner,此時并不會執行 inner 中的代碼,因為該函數還沒被調用,直到第3步
- 調用 inner 函數,執行 inner 中的代碼邏輯。
閉包
再來看一個例子:
- def outer(x):
- def inner():
- print(x)
- return inner
- closure = 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 inner
- def foo():
- print("foo")
- foo = outer(foo)
- foo()
我沒有修改 foo 函數里面的任何邏輯,只是給 foo 變量重新賦值了,指向了一個新的函數對象。***調用 foo(),不僅能打印日志,業務邏輯也執行完了。現在來分析一下它的執行流程。
這里的 outer 函數其實就是一個裝飾器,裝飾器是一個帶有函數作為參數并返回一個新函數的閉包,本質上裝飾器也是函數。outer 函數的返回值是 inner 函數,在 inner 函數中,除了執行日志操作,還有業務代碼,該函數重新賦值給 foo 變量后,調用 foo() 就相當于調用 inner()
foo 重新賦值前:
重新賦值后:
另外,Python為裝飾器提供了語法糖 @,它用在函數的定義處:
- @outer
- def foo():
- print("foo")
- foo()
這樣就省去了手動給foo重新賦值的步驟。
到這里不知你對裝飾器理解了沒有?當然,裝飾器還可以更加復雜,比如可以接受參數的裝飾器,基于類的裝飾器等等。
【本文是51CTO專欄作者“劉志軍”的原創文章,作者微信公眾號:Python之禪(VTtalk)】