Python裝飾器爲何難理解?

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

但爲何初學者對裝飾器的理解如此困難,我認爲本質上是對Python函數理解不到位,由於裝飾器本質上仍是函數python

函數定義

理解裝飾器前,須要明白函數的工做原理,咱們先從一個最簡單函數定義開始:面試

def foo(num):
    return num + 1複製代碼

上面定義了一個函數,名字叫foo,也能夠把 foo 可理解爲變量名,該變量指向一個函數對象緩存

調用函數只須要給函數名加上括號並傳遞必要的參數(若是函數定義的時候有參數的話)閉包

value = foo(3)
print(value) # 4複製代碼

變量名 foo 如今指向 <function foo at 0x1030060c8> 函數對象,但它也能夠指向另一個函數。函數

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() 的返回值是同一個對象。 測試

函數做爲參數

函數還能夠像整數同樣做爲函數的參數,例如:spa

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

函數嵌套

函數不只能夠做爲參數和返回值,函數還能夠定義在另外一個函數中,做爲嵌套函數存在,例如:

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

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

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

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

foo()複製代碼

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

到這裏不知你對裝飾器理解了沒有?固然,裝飾器還能夠更加複雜,好比能夠接受參數的裝飾器,基於類的裝飾器等等。下一篇能夠寫寫裝飾器的應用場景。

同步發表於:foofish.net/understand-…

公衆號:python之禪
相關文章
相關標籤/搜索