Python進階 - 裝飾器

函數(shù)進階知識

函數(shù)名只是一個指向函數(shù)的變量

在python中,一切皆對象。函數(shù)名只是一個指向函數(shù)的變量,為了驗證這一點,我們可以查看修改和刪除函數(shù)名對程序的影響:

def someFunc():  # someFunc只是一個變量名,它指向了我們創(chuàng)建的函數(shù)
    print("Now in a function")


if __name__ == '__main__':
    anotherFunc = someFunc  # 用另一個變量名指向剛創(chuàng)建的函數(shù)
    anotherFunc()  # 輸出Now in a function
    # 刪除了一個指向函數(shù)的變量名,對函數(shù)本身并沒有影響;
    # 因為還有一個變量anotherFunc指向函數(shù),因此函數(shù)本體并不會被回收
    del someFunc
    anotherFunc()  # 還是輸出Now in a function

可以看到,在python中函數(shù)名和一個普通變量一樣,只不過它指向了一組語句。

嵌套函數(shù)和變量作用域

在函數(shù)內(nèi)是可以定義另一個函數(shù)的,這叫做嵌套函數(shù)。作用域是程序運行時變量可被訪問的范圍,定義在函數(shù)內(nèi)的變量是局部變量,局部變量的作用范圍只能是函數(shù)內(nèi)部范圍內(nèi),它不能在函數(shù)外引用。根據(jù)這個原理,對于嵌套函數(shù)來說,在內(nèi)層的函數(shù)可以訪問外層函數(shù)定義的變量。

如下例所示:

def nested_func():
    msg = "This is a message"

    # 在函數(shù)內(nèi)部定義一個函數(shù),打印消息
    def print_message():
        # 在嵌套函數(shù)內(nèi)層可以訪問外層函數(shù)的變量
        print(msg)

    # 調(diào)用打印消息的函數(shù)
    print_message()  # output: This is a message


if __name__ == '__main__':
    nested_func()

將函數(shù)作為返回值

函數(shù)由于是一個對象,它是可以作為另一個函數(shù)的返回值或者作為參數(shù)傳遞給其他函數(shù)。

def nested_func(msg):
    # 在函數(shù)內(nèi)部定義一個函數(shù),打印消息
    def print_message():
        # 在嵌套函數(shù)內(nèi)層可以訪問外層函數(shù)的變量
        print(msg)

    return print_message


if __name__ == '__main__':
    msg = "This is a message"
    aFunction = nested_func(msg)  # 將aFunction指向了nested_Func(msg)的返回值,即print_message
    aFunction()  # 等同于調(diào)用了print_message()

這里將返回的函數(shù)賦值給另一個函數(shù),這樣可以實現(xiàn)對內(nèi)部函數(shù)從外部進行傳值。

閉包(Closure)

什么是閉包

兩個嵌套的函數(shù),內(nèi)部函數(shù)使用外部函數(shù)的變量。這套變量+內(nèi)部函數(shù)整體,就叫做閉包。

閉包避免了使用全局變量,此外,閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)連起來。這一點與面向?qū)ο缶幊淌欠浅n愃频?,在面對象編程中,對象允許我們將某些數(shù)據(jù)(對象的屬性)與一個或者多個方法相關(guān)聯(lián)。

為什么需要閉包

剛才提到過,閉包可以實現(xiàn)對內(nèi)部函數(shù)從外部進行傳值。實現(xiàn)這種功能,有兩套方案:使用類和使用閉包。使用閉包的方案我們前面已經(jīng)介紹過,我們下面看看如何用類實現(xiàn)這個功能:

class someClass(object):
    def __call__(self, msg):
        print(msg)


if __name__ == '__main__':
    msg = "This is a message"
    aFunction = someClass()
    aFunction(msg) # 輸出 This is a message

這里就實現(xiàn)了和上面用嵌套函數(shù)一樣的功能,在這里這個aFunction雖然是一個實例對象,但是我們定義了__call__方法之后,它也可以像函數(shù)一樣被調(diào)用。

這種實現(xiàn)方式的缺陷在于,python3的類默認(rèn)會繼承object(Python2 略有不同,繼承了object的叫做新式類,沒有繼承的叫做經(jīng)典類),它里面自帶了一系列我們用不到的方法,會占用相對較大的空間。

print(dir(aFunction))
# 輸出結(jié)果:
# ['__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

而閉包則更加精簡,占用的空間更小。

裝飾器的內(nèi)部執(zhí)行原理

什么是裝飾器

裝飾器就是一種閉包,只是這個閉包的函數(shù)名等于原先的函數(shù)名,從而改變了原先函數(shù)的指向。

def my_decorator(func):
    def print_msg():
        print("new message has been added!")
        func()

    return print_msg


def plain_print_msg():
    print("entering plain print msg")


if __name__ == '__main__':
    # plain_print_msg作為實參被傳給my_decorator
    # aFunc指向my_decorator的返回值,也就是print_msg
    aFunc = my_decorator(plain_print_msg)
    # 相當(dāng)于在指向print_msg,其內(nèi)部的func = plain_print_msg
    aFunc()
    print("-" * 10)

    # 將原先的函數(shù)名plain_print_msg直接指向my_decorator的返回值
    # 可以看到和上面部分的代碼實現(xiàn)的功能是相同的
    plain_print_msg = my_decorator(plain_print_msg)
    plain_print_msg()

可以看到,執(zhí)行下面兩行代碼之后,函數(shù)名稱并沒有變, 但是卻指向了一個新的函數(shù),在新的函數(shù)內(nèi),除了執(zhí)行原函數(shù)之外,還加了一些其他的功能,這就是裝飾器的實現(xiàn)原理。

在實際使用裝飾器時,我們并不需要手動修改函數(shù)名的指向,而是可以由@運算符來進行代勞,上面的代碼可以改寫如下:

def my_decorator(func):
    def print_msg():
        print("new message has been added!")
        func()

    return print_msg


@my_decorator
def plain_print_msg():
    print("entering plain print msg")


if __name__ == '__main__':
    plain_print_msg()

這里的@my_decorator和我們上面手動實現(xiàn)的代碼功能是一樣的。

為什么需要使用裝飾器

那么在開發(fā)過程中進行功能擴充時,為什么不直接修改函數(shù)源碼,而要使用裝飾器呢?

開放封閉原則:已經(jīng)實現(xiàn)的功能代碼不允許修改,但是可以被擴展。也即已經(jīng)實現(xiàn)的功能代碼塊封閉,而對擴展開發(fā)開放。這是因為在比較大的系統(tǒng)中,可能有很多模塊使用同一個函數(shù),如果為了修改功能,直接對函數(shù)進行修改,那么其他地方對函數(shù)的引用可能會出錯。而使用了裝飾器之后,可以在裝飾器內(nèi)梳理代碼邏輯,這樣不該動其他地方的代碼也可以正常加上新功能。

裝飾器什么時候開始生效的

裝飾器的生效不會等到函數(shù)進行調(diào)用,而是在解釋器讀到裝飾符開始,就會對函數(shù)進行裝飾。為了說明這一點,我們可以看下面這一段代碼:

def my_decorator(func):
    print("entering my decorator!")

    def print_msg():
        print("new message has been added!")
        func()

    return print_msg


@my_decorator
def plain_print_msg():
    print("entering plain print msg")


if __name__ == '__main__':
    pass # 注意這里并沒有執(zhí)行函數(shù)調(diào)用

輸出結(jié)果:

entering my decorator!

可以看到函數(shù)并沒有被調(diào)用,但是裝飾器已經(jīng)被加裝在函數(shù)之上了。

對帶參數(shù)的函數(shù)進行裝飾

通過上面對函數(shù)的學(xué)習(xí),我們已經(jīng)知道裝飾器的工作原理了,因此如果需要對帶參數(shù)的函數(shù)進行裝飾,無非就是在閉包的內(nèi)層函數(shù)上加上相應(yīng)的參數(shù)。示例如下:

import time


def time_wrapper(func):
    def timer(cnt):
        start_time = time.time()
        func(cnt)
        end_time = time.time()
        print("time consumed by %s: %s second" % (func.__name__, str(end_time - start_time)))

    return timer


@time_wrapper
def count1(cnt):
    lst = []
    for i in range(cnt):
        lst.insert(0, i)


@time_wrapper
def count2(cnt):
    lst = []
    for i in range(cnt):
        lst.append(i)


if __name__ == '__main__':
    count1(10000)
    count2(10000)

對有返回值的函數(shù)進行裝飾

在了解了裝飾器的原理之后,我們就應(yīng)該知道如果原函數(shù)帶有返回值的話,我們需要在裝飾器的什么地方進行返回了:

def my_decorator(func):
    print("entering my decorator!")

    def print_msg():
        print("new message has been added!")
        return func() # 將函數(shù)的返回值作為返回值

    return print_msg


@my_decorator
def plain_print_msg():
    print("entering plain print msg")
    return "OK"  # 函數(shù)帶有返回值,指示打印狀態(tài)


if __name__ == '__main__':
    # 這里給的實際上是print_msg()的返回值,也就是func()
    # 形參func = plain_print_msg,因此返回值和原函數(shù)plain_print_msg的返回值是相同的
    ret = plain_print_msg() 
    print(ret)

多個裝飾器裝飾同一個函數(shù)

當(dāng)有多個裝飾器裝飾同一個函數(shù)時,最靠近函數(shù)的(位于程序最下端的)裝飾器最先裝飾,然后依次由近及遠進行裝飾。而調(diào)用閉包時,由遠及近進行調(diào)用。

def decorator1(func):
    print("entering decorator1")

    def add_msg():
        print("using decorator1: add_msg")
        func()

    return add_msg


def decorator2(func):
    print("entering decorator2")

    def add_another_msg():
        print("using decorator2: add_another_msg")

    return add_another_msg


@decorator1  # 距離函數(shù)較遠的裝飾器后裝飾,先調(diào)用(包在外層)
@decorator2  # 距離函數(shù)較近的裝飾器先裝飾,后調(diào)用(包在內(nèi)層)
def plain_print_msg():
    print("entering plain print msg")
    return "OK"  # 函數(shù)帶有返回值,指示打印狀態(tài)


if __name__ == '__main__':
    plain_print_msg()

輸出的結(jié)果:

entering decorator2
entering decorator1
using decorator1: add_msg
using decorator2: add_another_msg

用類實現(xiàn)裝飾器

明白了裝飾器的實現(xiàn)原理之后,就可以知道用類也是可以實現(xiàn)裝飾器的。這就是我們之前講的用類實現(xiàn)閉包功能的@裝飾符版本。

class Test():
    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        print("----調(diào)用裝飾器----")
        self._func(*args, **kwargs)


@Test
def my_fun(a, b, c):
    print(a, b, c)


if __name__ == '__main__':
    my_fun(10, 20, 'main')

輸出結(jié)果:

----調(diào)用裝飾器----
10 20 main

帶參數(shù)的裝飾器

當(dāng)裝飾器帶有參數(shù)時,調(diào)用過程分為兩步:

  • 調(diào)用裝飾器函數(shù)(閉包外層的函數(shù))并將參數(shù)作為實參傳遞
  • 用上一步的返回值當(dāng)作裝飾器對函數(shù)進行裝飾

因此需要給裝飾器加上參數(shù)時,我們要對閉包進行再一層包裝,在最外層傳遞入裝飾器中傳進來的參數(shù)。

一個對操作進行權(quán)限檢查的例子如下:

def get_level(level):
    def level_check_wrapper(func):
        def level_check():
            print("正在檢查權(quán)限:")
            if level == 1:
                print("權(quán)限等級為1")
                return func()
            elif level == 2:
                print("權(quán)限等級為2")
                return func()

        return level_check

    return level_check_wrapper


@get_level(1)
def low_level_op():
    print("低級權(quán)限操作")
    print([i for i in range(5)])


@get_level(2)
def high_level_op():
    print("高級權(quán)限操作")
    print([item for item in "abcdefgh"])


if __name__ == '__main__':
    low_level_op()
    high_level_op()
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 裝飾器在日志、緩存等應(yīng)用中有廣泛使用,我們首先從之前講解的閉包為出發(fā)點,給大家講解裝飾器的那些事。 簡單的裝...
    羅羅攀閱讀 771評論 0 8
  • python裝飾器是在函數(shù)調(diào)用之上的修飾,這些修飾是在聲明或者定義一個函數(shù)的時候進行設(shè)置的。同時,裝飾器是一個返回...
    happy_19閱讀 680評論 0 5
  • 這篇文章要解決的問題: # 裝飾器是什么? # 裝飾器的種類? # 為什么使用裝飾器? # 怎么使用裝飾器? # ...
    Nietzsche_LiBai閱讀 528評論 0 2
  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔,今天18年5月份再次想寫文章,發(fā)現(xiàn)簡書還為我保存起的...
    Jenaral閱讀 3,162評論 2 9
  • 部分細節(jié)自己改了點,也加了點自己例子,基本上屬于轉(zhuǎn)載。轉(zhuǎn)載出處:https://my.oschina.net/le...
    洛克黃瓜閱讀 2,115評論 0 3

友情鏈接更多精彩內(nèi)容