1. 程式人生 > >Python裝飾器為什麼難理解?

Python裝飾器為什麼難理解?

無論專案中還是面試都離不開裝飾器話題,裝飾器的強大在於它能夠在不修改原有業務邏輯的情況下對程式碼進行擴充套件,許可權校驗、使用者認證、日誌記錄、效能測試、事務處理、快取等都是裝飾器的絕佳應用場景,它能夠最大程度地對程式碼進行復用。

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

函式定義

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

def foo(num):
    return num + 1

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

python-functioin1.png

呼叫函式只需要給函式名加上括號並傳遞必要的引數(如果函式定義的時候有引數的話)

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

變數名 foo 現在指向 <function foo at 0x1030060c8> 函式物件,但它也可以指向另外一個函式。

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

function4.png

函式作為返回值

在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() 的返回值是同一個物件。

python-totur.png

函式作為引數

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

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)。

python-function2.png

函式巢狀

函式不僅可以作為引數和返回值,函式還可以定義在另一個函式中,作為巢狀函式存在,例如:

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 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 重新賦值前:

function-decorator.png

重新賦值後,foo = outer(foo)

function-decorator2.png

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

@outer
def foo():
    print("foo")

foo()

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

到這裡不知你對裝飾器理解了沒有?當然,裝飾器還可以更加複雜,比如可以接受引數的裝飾器,基於類的裝飾器等等。下一篇可以寫寫裝飾器的應用場景。


關注公眾號「Python之禪」(id:vttalk)獲取最新文章 python之禪