1. 程式人生 > >Python 閉包 & 裝飾器

Python 閉包 & 裝飾器

目錄

閉包

Closure: 如果內層函式引用了外層函式的區域性變數(L),並且在外層函式中 return 內層函式時,這種關係就稱之為閉包。 閉包的特點就是返回的內層函式還引用了外層函式的區域性變數,所以要想正確的使用閉包,那麼就要確保這個被內層函式引用的區域性變數是不變的。 EXAMPLE:

In [71]: def count():
    ...:     fs = []
    ...:     for i in range(1, 4):
    ...:         def f():
    ...:              return i*i
    ...:         fs.append(f)
    ...:     return fs
    ...:
    ...: f1, f2, f3 = count()
    ...:

In [72]: f1,f2,f3
Out[72]: (<function __main__.f>, <function __main__.f>, <function __main__.f>)

In [73]: f1(),f2(),f3()
Out[73]: (9, 9, 9)

最總 f1(),f2(),f3() 輸出的是 (9,9,9) 而不是 (1,4,9)。 原因就是當 count() 函式返回了 f1(),f2(),f3() 3個函式時,這3個函式所引用的變數 i 的值已經變成了 3。由於 f1、f2、f3 並沒有被呼叫,所以,此時他們並未計算 i*i 。返回函式不要引用任何迴圈變數,或者後續會發生變化的變數。

函式的實質和屬性

  • 函式是一個物件,在記憶體中會佔用儲存空間
  • 一般來說函式執行完成後其內部的變數都會被回收。但如果函式中的變數被 return 時,那麼這個變數是不會被回收的,因為其引用計數 != 0
  • 函式具有屬性
  • 函式具有返回值
  • 函式通過函式名來引用

EXAMPLE:

passline = 60
def func(val):
    print 'id(val): %x' % id(val)
    if val >= passline:
        print "pass"
    else:
        print "failed"

    def in_func():
        print val

    in_func()
    return in_func

f = func(89)
f()

print (f.__closure__)

Output:

In [33]: run demo_1.py
id(val): 33c6548     # 變數 val 在記憶體中的地址
pass
89
(<cell at 0x0000000004E22588: int object at 0x00000000033C6548>,)  # 函式 f 包含了一個 int 物件,這個 int 物件的引用跟變數 val 是相同的

NOTE1: in_func() 引用了巢狀作用域(E)中的變數 valNOTE2: 並且內層函式 in_func() 被外層函式 func() return。 所以,val 這個變數被包含在了函式 in_func() 中一起被返回給了變數 f,即:變數 val 被變數 f 所引用,所以這個變數 val 並沒有在函式 func() 執行結束之後被回收。

總結: 外層函式區域性作用域中的變數能夠被內部函式所引用。

閉包有什麼好處?

因為閉包是通過 return 一個函式來定義的,所以我們可以把一些函式延遲執行。EXAMPLE:

def calc_sum(lst):
    def lazy_sum():
        return sum(lst)
    return lazy_sum

呼叫calc_sum()並沒有計算出結果,而是返回函式:

>>> func = calc_sum([1, 2, 3, 4])
>>> func
<function lazy_sum at 0x1037bfaa0>

對返回的函式進行呼叫時,才計算出結果:

>>> func()
10

由於可以返回函式,所以我們在後續程式碼裡就可以靈活的決定到底要不要呼叫該返回的函式。

再看下面一個例子:

def set_passline(passline):
    def cmp(val):
        if val >= passline:
            print "pass"
        else:
            print "failed"
    return cmp

f_100 = set_passline(60)
f_100(89)
print "f_100 type: %s" % type(f_100)
print "f_100 param: %s" % f_100.__closure__
print '*' * 20
f_150 = set_passline(90)
f_150(89)

Output:

In [37]: run demo_1.py
pass
f_100 type: <type 'function'>
f_100 param: <cell at 0x0000000004D5A468: int object at 0x00000000033C6038>
********************
failed

set_passline() 和 cmp() 組成一個閉包,最明顯的好處在於可以提高 cmp() 函式內程式碼實現的複用性和封裝(內層函式不能在全域性作用域中被直接呼叫)。不需要為兩個不用的判斷(100 | 150)而寫兩個相似的函式。

而且外層函式中的區域性變數還可以是一個函式。 繼續看下一個例子:

def my_sum(*arg):
    print "my_sum"
    print arg
    return sum(arg)

def my_average(*arg):
    return sum(arg)/len(arg)

def dec(func):
    print "dec"
    print func
    def in_dec(*arg): 
        print "in_dec"
        print func
        print arg
        if len(arg) == 0:
            return 0
        for val in arg:
            if not isinstance(val, int):
                return 0
        return func(*arg)
    return in_dec

# dec(my_sum)(1, 3, 4)
my_sum_result = dec(my_sum)
print my_sum_result(1, 3, 4)
print my_sum_result.__closure__

print '*' * 40

my_average_result = dec(my_average)
print my_average_result(1, 3, 4)
print my_average_result.__closure__

Output:

In [35]: run demo_1.py
dec
<function my_sum at 0x00000000048F1208>
in_dec
<function my_sum at 0x00000000048F1208>
(1, 3, 4)
my_sum
(1, 3, 4)
8
(<cell at 0x0000000004A7C618: function object at 0x00000000048F1208>,)
****************************************
dec
<function my_average at 0x00000000048F1A58>
in_dec
<function my_average at 0x00000000048F1A58>
(1, 3, 4)
2
(<cell at 0x0000000004A7CE58: function object at 0x00000000048F1A58>,)

小結

  • 閉包能夠提高程式碼的複用和內層函式的封裝
  • 內層函式引用外層函式的區域性變數可以是基本資料型別物件,也可以是一個函式物件
  • 呼叫外層函式(傳遞外層形參)時,返回的是一個記憶體函式物件,然後可以再對內層函式傳遞形參,這樣就增強了程式碼的靈活性。EG. 由外層設定條件,內層進行處理。

當然我們可以能回認為這樣的寫法並不美觀,所以我們繼續看 Python 的裝飾器和閉包的關係。

裝飾器

裝飾器實際上就是一個函式, 而且是一個接收函式物件的函式. 有了裝飾器我們可以在執行被裝飾函式之前做一個預處理, 也可以在處理完函式之後做清除工作. 所以在裝飾器函式中你會經常看見這樣的程式碼: 函式體定義了一個內嵌函式, 並在內嵌函式內實現了目標函式(被裝飾函式)的呼叫.這是一種 AOP 面向方法程式設計的方式. - 裝飾器用於裝飾函式 ⇒ dec() 用於裝飾 my_sum() - 裝飾器返回一個新的函式物件 ⇒ dec() 返回一個函式 in_dec() - 被裝飾的函式識別符號指向返回的函式物件 ⇒ my_sum() 指向 in_dec() my_sum_result = dec(my_sum) 即: my_sum_resultdec() 返回的函式物件,引用指向了 my_sum() - 語法糖:@裝飾器名稱

從上面幾點特性可以看出,裝飾器就是對閉包的使用。將上面的例子轉換一下寫法:

def dec(func):
    print 'call dec()'
    def in_dec(*arg): 
        print "call in_dec()"
        if len(arg) == 0:
            return 0
        for val in arg:
            if not isinstance(val, int):
                return 0
        return func(*arg)
    return in_dec

# @dec 執行的過程:
# 1. dec(my_sum) --> return in_dec
# 2. my_sum = in_dec
# 為了豐富被裝飾函式的功能(這裡是豐富了一個引數驗證功能),是一種抽象的程式設計思維
@dec        
def my_sum(*arg):
    print 'call my_sum()'
    return sum(arg)

def my_average(*arg):
    return sum(arg)/len(arg)

print my_sum(1,2,3,4,5)

Output:

In [43]: run demo_1.py
call dec()
call in_dec()
call my_sum()
15

更加深入的看看裝飾器的執行過程

  • chocolate() 只是一個普通的函式
def chocolate():
    print "call chocolate()"
chocolate()

Output:

In [65]: run demo_1.py
call chocolate()
  • 1
  • 2

正常的呼叫了 chocolate() 函式

  • chocolate() 加上了一個沒有返回記憶體函式的裝飾器
def jmilkfan(func):
    def in_jmilkfan():
        print "call in_jmilkfan()"
        func()
    print "call jmilkfan()"

@jmilkfan
def chocolate():
    print "call chocolate()"
chocolate()

Output:

In [51]: run demo_1.py
call jmilkfan()  # 首先呼叫了 jmilkfan 裝飾器對應的 jmilkfan() 函式

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
D:\Development\Python27\workspace\demo_1.py in <module>()
      9     print "call chocolate()"
     10 #print 'typr chocolate:%s' % type(chocolate)
---> 11 chocolate()

TypeError: 'NoneType' object is not callable

**ERROR:**chocolate 是一個’NoneType’物件 因為 jmilkfan() 預設 return 了一個 None, 而不是內層函式 in_jmilkfan(),這裡說明裝飾器對應的 jmilkfan() 的確被呼叫了。但卻沒有返回內層函式給 chocolate 接收。

def jmilkfan(func):

    print 'func id:%x' % id(func)

    def in_jmilkfan():
        print "call in_jmilkfan()"
        func()

    print "call jmilkfan()"
    print 'in_jmilkfan():%s' % in_jmilkfan.__closure__
    return in_jmilkfan

@jmilkfan
def chocolate():
    print 'chocolate():%s' % chocolate.__closure__
    print "call chocolate()"
chocolate()

Output:

In [67]: run demo_1.py
func id:490ecf8
call jmilkfan()
in_jmilkfan():<cell at 0x0000000004B0F5B8: function object at 0x000000000490ECF8>
call in_jmilkfan()
chocolate():<cell at 0x0000000004B0F5B8: function object at 0x000000000490ECF8>
call chocolate()

可看出 choholate()、 in_jmilkfan()、 func 都是引用了同一個函式物件。即: in_jmilkfan() 被 chocolate() 接收了。

注意當被裝飾的函式擁有形參時,裝飾器的內層函式必須定義相同的形參,否則會報錯。

def jmilkfan(func):

    print 'func id:%x' % id(func)

    def in_jmilkfan(x, y):
        print "call in_jmilkfan()"
        func(x, y)

    print "call jmilkfan()"
    print 'in_jmilkfan():%s' % in_jmilkfan.__closure__
    return in_jmilkfan

@jmilkfan
def chocolate(x, y):
    print 'chocolate():%s' % chocolate.__closure__
    print "call chocolate(), the value is:%d" % (x + y)

chocolate(1, 2)

Output:

In [70]: run demo_1.py
func id:490edd8
call jmilkfan()
in_jmilkfan():<cell at 0x0000000004AF3288: function object at 0x000000000490EDD8>
call in_jmilkfan()
chocolate():<cell at 0x0000000004AF3288: function object at 0x000000000490EDD8>
call chocolate(), the value is:3

帶引數的裝飾器

@decomaker(deco_args)
def foo():pass

# 等效於

foo = decomaker(deco_args)(foo)

帶引數的裝飾器和不帶引數的裝飾器的區別在於, 前者需要先傳入一個引數並返回一個函式物件, 該函式物件才是實際作用於被裝飾函式的裝飾器.

裝飾器的疊加

@deco1(deco_args)
@deco2
def func():pass

#等效於

func = deco1(deco_args)(deco2(func))

小結

裝飾器的作用: 在提供了閉包的工作之外,優雅了語法

裝飾器的使用過程

  • 呼叫了裝飾器對應的外層函式,並且將被裝飾函式作為實參傳遞給外層函式。目的是為了能夠讓記憶體函式使用這個作為外層函式的區域性變數的被裝飾函式,從而執行被裝飾函式的程式碼實現。
  • 外層函式將內層函式的引用返回給被裝飾函式,實現閉包。

裝飾器能解決什麼問題?

問題:如果我們定義了一個函式後,又想在執行時動態的為這個函式增加功能,但是又不希望改動這個函式本身的程式碼?那麼我們能否將這個函式作為一個引數傳遞給另一個函式,從而產生一個擁有更多功能的新函式呢 ?

這就用到了所謂的高階函式: 1. 可以接受函式作為引數(函式又函式名來引用,函式名的本質就是一個變數) 2. 可以返回一個函式這樣高階函式就能夠到達我們希望接收一個函式併為其進行包裝後再返回一個新的函式的效果。 EXAMPLE:

func = new_func(func)
# new_func 是一個高階函式,也可以成為一個裝飾器
# 因為函式是通過函式名來引用的,所以我們可以將一個由高階函式返回的新的函式再次賦值給原來的函式名
# 這樣的話 func 的原始定義函式就被徹底隱藏起來了,達到了封裝的效果
# Python 還可以通過 @ 來簡化裝飾器的呼叫, EG:
@new_func
def func(x):
    ...

小結

裝飾器可以極大的簡化程式碼,避免每個函式編寫重複性的程式碼。 我們可以將需要被重複且希望動態呼叫的程式碼寫成裝飾器,EG: 引入日誌(列印日誌):@log 檢測效能(增加計時邏輯來監測效能):@performance 加入事務能力(資料庫事務):@transaction URL 路由:@post('/register')

全文地址請點選:https://blog.csdn.net/jmilk/article/details/52504950?utm_source=copy