函數進階(閉包)


閉包

  • 閉包函數的含義:1.內部函數引用外部函數變量      2.從內部函數返回一個值到全局
  • 簡單來說就是一個函數定義中引用了函數外定義的變量,並且該函數可以在其定義環境外被執行。這樣的一個函數我們稱之為閉包。實際上閉包可以看做一種更加廣義的函數概念。因為其已經不再是傳統意義上定義的函數。

一、閉包函數小例子:

def outer_func():
    loc_list = []
    def inner_func(name):
        loc_list.append(len(loc_list) + 1)
        print('%s loc_list = %s' %(name, loc_list))
    return inner_func
clo_func_0 = outer_func()
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_1 = outer_func()
clo_func_1('clo_func_1')
clo_func_1('clo_func_1')

程序運行結果: 

clo_func_0 loc_list = [1]
clo_func_0 loc_list = [1, 2]
clo_func_0 loc_list = [1, 2, 3]
clo_func_0 loc_list = [1, 2, 3, 4]
clo_func_1 loc_list = [1]
clo_func_1 loc_list = [1, 2]

  這是一個閉包函數,但是引用的變量是個列表型,也就是說這個變量是一個可變類型,每次調用函數,其結果都會變化。

同下例的簡單函數:

#函數的魔性寫法
def func(lst=[]):
    lst.append(1)
    print(lst)

# 默認參數盡量避免使用可變的類型
func()
func()
func()
func()

#調用結果
[1]
[1, 1]
[1, 1, 1]
[1, 1, 1, 1]

 

在閉包函數例子中我們至少可以對閉包中引用的自由變量有如下的認識:

  • 閉包中的引用的自由變量只和具體的閉包有關聯,閉包的每個實例引用的自由變量互不干擾。
  • 一個閉包實例對其自由變量的修改會被傳遞到下一次該閉包實例的調用。

由於這個概念理解起來並不是那么的直觀,因此使用的時候很容易掉進陷阱。

二、閉包陷阱

閉包函數陷阱:

def my_func(*args):
    fs = []
    for i in range(3):
        def func():
            return i * i
        fs.append(func)
    return fs

fs1, fs2, fs3 = my_func()
print (fs1())
print (fs2())
print (fs3())

#運行結果
4
4
4

  上面這段代碼可謂是典型的錯誤使用閉包的例子。程序的結果並不是我們想象的結果0,1,4。實際結果全部是4。

這個例子中,my_func返回的並不是一個閉包函數,而是一個包含三個閉包函數的一個list。這個例子中比較特殊的地方就是返回的所有閉包函數均引用父函數中定義的同一個自由變量。

  但這里的問題是為什么for循環中的變量變化會影響到所有的閉包函數?尤其是我們上面剛剛介紹的例子中明明說明了同一閉包的不同實例中引用的自由變量互相沒有影響的。而且這個觀點也絕對的正確。

  那么問題到底出在哪里?應該怎樣正確的分析這個錯誤的根源。

  其實問題的關鍵就在於在返回閉包列表fs之前for循環的變量的值已經發生改變了,而且這個改變會影響到所有引用它的內部定義的函數。因為在函數my_func返回前其內部定義的函數並不是閉包函數,只是一個內部定義的函數。

  當然這個內部函數引用的父函數中定義的變量也不是自由變量,而只是當前block中的一個local variable。

def my_func(*args):
    fs = []
    j = 0
    for i in range(3):
        def func():
            return j * j
        fs.append(func)
    j = 2
    return fs
f = my_func()

print(f)

#運行結果:
[<function my_func.<locals>.func at 0x00000000021107B8>, <function my_func.<locals>.func at 0x0000000002110840>,
<function my_func.<locals>.func at 0x00000000021108C8>]

  

  上面的這段代碼邏輯上與之前的例子是等價的。這里或許更好理解一點,因為在內部定義的函數func實際執行前,對局部變量j的任何改變均會影響到函數func的運行結果。

  函數my_func一旦返回,那么內部定義的函數func便是一個閉包,其中引用的變量j成為一個只和具體閉包相關的自由變量。后面會分析,這個自由變量存放在Cell對象中。

  使用lambda(蘭布達)表達式重寫這個例子:

def my_func(*args):
    fs = []
    for i in range(3):
        func = lambda : i * i
        fs.append(func)
    return fs

  

  經過上面的分析,我們得出下面一個重要的經驗:返回閉包中不要引用任何循環變量,或者后續會發生變化的變量。

  這條規則本質上是在返回閉包前,閉包中引用的父函數中定義變量的值可能會發生不是我們期望的變化。

def my_func(*args):
    fs = []
    for i in range(3):
        def func(_i = i):
            return _i * _i
        fs.append(func)
    return fs

#或者
def my_func(*args):
fs = []
for i in range(3):
func = lambda _i = i : _i * _i
fs.append(func)
return fs

  

  正確的做法便是將父函數的local variable賦值給函數的形參。函數定義時,對形參的不同賦值會保留在當前函數定義中,不會對其他函數有影響。

  另外注意一點,如果返回的函數中沒有引用父函數中定義的local variable,那么返回的函數不是閉包函數。

閉包函數的應用:https://www.cnblogs.com/yssjun/p/9887239.html

 

 

 

 

 

 

 

 

 

 

  

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM