1. 程式人生 > >Python三大神器之裝飾器

Python三大神器之裝飾器

在之前的文章中寫了Python的生成器和迭代器,今天給大家分享一下關於裝飾器的一些知識。

閉包

在講裝飾器之前一定要提及的就是閉包,因為Python中的閉包是實現裝飾器的基礎。

# 定義一個函式
def test(number):

    # 在函式內部再定義一個函式,並且這個函式用到了外邊函式的變數,那麼將這個函式以及用到的一些變數稱之為閉包
    def test_in(number_in):
        print("in test_in 函式, number_in is %d" % number_in)
        return number+number_in
    # 其實這裡返回的就是閉包的結果,也就是返回test_in這個內部函式的引用
    return test_in


# 給test函式賦值,這個20就是給引數number
# 這裡的ret實際上接收的是執行完test之後的test_in的引用
ret = test(20)

# 注意這裡的100其實給引數number_in,ret(100)在這裡等價於test_in(100)
print(ret(100))

#注 意這裡的200其實給引數number_in
print(ret(200))
執行結果:


in test_in 函式, number_in is 100
120

in test_in 函式, number_in is 200
220

從上面的例子我們可以得出一些結論:

閉包= 內部函式 + 自由變數
閉包的3個特點:
1 函式的巢狀定義
2 外部函式返回內部函式的引用
3 內部函式可以使用外部函式提供的自由變數

閉包的另一個例項:

def line_conf(a, b):
    def line(x):
        return a*x + b
    return line
#注意這裡的line1和line2是兩個獨立執行的空間互不影響
line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5))
print(line2(5))

這個例子中,函式line與變數a,b構成閉包。在建立閉包的時候,我們通過line_conf的引數a,b說明了這兩個變數的取值,這樣,我們就確定了函式的最終形式(y = x + 1和y = 4x + 5)。我們只需要變換引數a,b,就可以獲得不同的直線表達函式。由此,我們可以看到,閉包也具有提高程式碼可複用性的作用。
如果沒有閉包,我們需要每次建立直線函式的時候同時說明a,b,x。這樣,我們就需要更多的引數傳遞,也減少了程式碼的可移植性。
注意點:
由於閉包引用了外部函式的區域性變數,則外部函式的區域性變數沒有及時釋放,消耗記憶體。

修改外部函式的變數

#python3的方法
def counter(start=0):
    def incr():
        nonlocal start 
        start += 1
        return start
    return incr
c1 = counter(5)
print(c1())
#python2 的方法 python2的方法實際並沒有修改start的值,只是利用了bug.

def counter(start=0):
    count = [start]
    def incr():
        
        count[0] += 1
        return count[0]
    return incr
c1 = counter(5)
print(c1())

閉包的總結:

函式名只是函式程式碼空間的引用,當函式名賦值給一個物件的時候 就是引用傳遞
閉包就是一個巢狀定義的函式,在外層執行時才開始內層函式的定義,然後將內部函式的引用傳遞函式外的物件

內部函式和使用的外部函式提供的變數構成的整體稱為閉包

裝飾器

在初步瞭解完畢閉包之後,我們就可以來看看裝飾器到底是什麼樣的了。

裝飾器是程式開發中經常會用到的一個功能,用好了裝飾器,開發效率如虎添翼,所以這也是Python面試中必問的問題。

裝飾器相對於其他是一個比較難明白的一個知識點,但是這是開發必備的基礎,也是作為一個python程式設計師必須掌握的。

#### 1 ####
def foo():
    print('foo')

foo  # 表示是函式
foo()  # 表示執行foo函式

#### 2 ####
def foo():
    print('foo')

foo = lambda x: x + 1

foo()  # 執行的是lambda表示式,而不再是原來的foo函式,因為foo這個名字被重新指向了另外一個匿名函式,也就是foo成為了這個匿名函式的引用

裝飾器究竟是為了實現什麼功能而被開發出來的呢?

寫程式碼要遵循開放封閉原則,雖然在這個原則是用的面向物件開發,但是也適用於函數語言程式設計,簡單來說,它規定已經實現的功能程式碼不允許被修改,但可以被擴充套件,即:

封閉:已實現的功能程式碼塊
開放:對擴充套件開發

根據這個原則如果要在函式模組新增功能就不能在內部再新增程式碼了,這裡裝飾器就派上用場了。

我們在下面的程式碼中會將f1函式新增一個驗證的功能:

def yanzheng(func):
    """裝飾器函式的特點: 基於閉包實現的 有且只有一個引數用於儲存被裝飾的函式的引用"""
    #這個引數接收的一定是被增加功能的函式的引用
    def inner():
        print("驗證1......")
        func()
    return inner

# 在不修改程式碼的情況下 對程式碼進行功能的擴充套件
# 裝飾過程: 1 把需要被裝飾的函式的引用傳入到 裝飾器函式內部儲存 func中
# 2 建立內部函式 在其中呼叫func
# 3 在呼叫func之前可以擴充套件 新功能
# 4 將裝飾器函式內部的 內部函式的引用傳給 f1<此時的f1就是裝飾功能後的f1>
# 5 對於使用者來講呼叫f1就可以呼叫 f1原有函式的功能
# 6 實際上呼叫f1是執行的inner函式  inner內部中儲存的有f1原有的引用

# @裝飾器函式名 語法只是一個語法糖 對靈魂程式碼的一種語法封裝
@yanzheng   #相當於f1 = yanzheng(f1)在這裡兩個f1是不一樣的,右邊f1是下面f1這個函式的引用,
而左邊f1在傳入yanzheng函式傳入f1引數之後成為了內部函式inner的引用。在inner函式中我們呼叫了原本的f1函式,並且附加了列印驗證的功能。
def f1():
    print("in f1")

f1()

無引數的函式

無引數的函式


from time import ctime, sleep

def timefun(func):
    def wrapped_func():
        print("%s called at %s" % (func.__name__, ctime()))
        func()
    return wrapped_func

@timefun
def foo():
    print("I am foo")

foo()
sleep(2)
foo()
上面程式碼理解裝飾器執行行為可理解成

foo = timefun(foo)
# foo先作為引數賦值給func後,foo接收指向timefun返回的wrapped_func
foo()
# 呼叫foo(),即等價呼叫wrapped_func()
# 內部函式wrapped_func被引用,所以外部函式的func變數(自由變數)並沒有釋放
# func裡儲存的是原foo函式物件

有引數的函式

被裝飾的函式有引數

from time import ctime, sleep

def timefun(func):
    def wrapped_func(a, b):
        print("%s called at %s" % (func.__name__, ctime()))
        print(a, b)
        func(a, b)
    return wrapped_func

@timefun
def foo(a, b):
    print(a+b)

foo(3,5)
sleep(2)
foo(2,4)

被裝飾的函式有不定長引數

from time import ctime, sleep

def timefun(func):
    def wrapped_func(*args, **kwargs):
        print("%s called at %s"%(func.__name__, ctime()))
        func(*args, **kwargs)
    return wrapped_func

@timefun
def foo(a, b, c):
    print(a+b+c)

foo(3,5,7)
sleep(2)
foo(2,4,9)

裝飾器中的return

from time import ctime, sleep

def timefun(func):
    def wrapped_func():
        #先用info 接收一下info 的返回值等待之後的新增的功能執行完畢後再返回
        info = func()
        print("%s called at %s" % (func.__name__, ctime()))
        return info
    return wrapped_func

@timefun
def foo():
    print("I am foo")

@timefun
def getInfo():
    return '----hahah---'

foo()
sleep(2)
foo()


print(getInfo())

裝飾器帶引數,在原有的基礎上,新增外部變數。

from time import ctime, sleep

def timefun_arg(pre="hello"):
    def timefun(func):
        def wrapped_func():
            print("%s called at %s %s" % (func.__name__, ctime(), pre))
            return func()
        return wrapped_func
    return timefun

# 下面的裝飾過程
# 1. 呼叫timefun_arg("python")
# 2. 將步驟1得到的返回值,即time_fun返回, 然後time_fun(foo)
# 3. 將time_fun(foo)的結果返回,即wrapped_func
# 4. 讓foo = wrapped_fun,即foo現在指向wrapped_func
# 過程整體可以理解為:把timefun_arg('python')當做一個外部函式的整體,在執行這個函式之後,這個整體就相當於timefun(func),就又變回了我們的基本裝飾器的定義了。而傳入的python引數也可以作為一個自由變數傳入內部函式當中使用。
@timefun_arg("python")  #foo = timefun_arg('python')(foo)
def foo():
    print("I am foo")



foo()
sleep(2)
foo()

可以理解為

foo()==timefun_arg("itcast")(foo)()

類裝飾器


class Test(object):
    def __init__(self, func):
        print("---初始化---")
        print("func name is %s"%func.__name__)
        self.__func = func
    def __call__(self):
        print("---裝飾器中的功能---")
        self.__func()
#說明:
#1. 當用Test來裝作裝飾器對test函式進行裝飾的時候,首先會建立Test的例項物件
#   並且會把test這個函式名當做引數傳遞到__init__方法中
#   即在__init__方法中的屬性__func指向了test指向的函式
#
#2. test指向了用Test創建出來的例項物件
#
#3. 當在使用test()進行呼叫時,就相當於讓這個物件(),因此會呼叫這個物件的__call__方法
#
#4. 為了能夠在__call__方法中呼叫原來test指向的函式體,所以在__init__方法中就需要一個例項屬性來儲存這個函式體的引用
#   所以才有了self.__func = func這句程式碼,從而在呼叫__call__方法中能夠呼叫到test之前的函式體
@Test
def test():
    print("----test---")
test()
showpy()#如果把這句話註釋,重新執行程式,依然會看到"--初始化--"

多個裝飾器裝飾一個函式(較難理解)

# 定義函式:完成包裹資料
def makeBold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

# 定義函式:完成包裹資料
def makeItalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makeBold
def test1():
    return "hello world-1"

@makeItalic
def test2():
    return "hello world-2"

@makeBold
@makeItalic
def test3():
    return "hello world-3"
#這裡我們拆分理解下
test3 = makeBold(test3)
test3 = makeItalic(test3)
程式的執行過程如下,程式會就近原則先執行test3 = makeItalic(test3)這個裝飾器,那麼執行出來的結果test3會是makeItalic中的weapped()的引用,再返回執行上一行的test3 = makeBold(test3),這裡加入的引數test3已經變成了makeItalic中的wrapped()的引用,最後test3指向的是warpped()的引用。
所以在執行test3()的時候的執行流程為,先執行<b>,然後再執行fn()而此時的fn指向的是makeItalic中的weapped()的引用,接著就會執行<i>然後執行makeItalic的fn,輸出結果hello world-3,fn執行完了再執行</i>注意此時只是makeItalic中warapped()的fn執行完了,哦後面還有</i>將其輸出,此時全部返回。就得到了下面的結果:<b><i>hello world-3</i></b>。

總結出來就是:在裝飾器中實際的程式碼是從下到上,實現的過程是從上到下。

print(test1())
print(test2())
print(test3())
執行結果:

<b>hello world-1</b>
<i>hello world-2</i>
<b><i>hello world-3</i></b>

總結:

  • 裝飾器函式只有一個引數就是被裝飾的函式的應用
  • 裝飾器能夠將一個函式的功能在不修改程式碼的情況下進行擴充套件
  • 在函式定義的上方@裝飾器函式名 即可直接使用裝飾器對下面的函式進行裝飾。