【Python語法】循序漸進理解閉包

Sarah_mq發表於2020-11-26

1. 閉包初接觸

在一個內部函式中,對外部作用域的變數進行引用,(並且一般外部函式的返回值為內部函式),那麼內部函式就被認為是閉包。

1.1 閉包基本語法結構:

# 外部函式返回內部函式
def outside(attr1):
    # 內部函式使用了外部函式的變數
    def inside(attr2):
        return attr1 + attr2
	# 外部函式返回內部函式
    return inside


# 因為outside()返回的是函式,所以a也是一個函式
a = outside(1)
print(a)
# 因此可以對函式傳入引數,按照函式的執行順序,返回
print(a(2))

結果:

<function outside..inside at 0x00000229B7D93828>
3

通過閱讀程式碼註釋,可以理解最終的執行結果,但是要深入理解閉包,還需要往下看

1.2 明確變數作用域:

閉包中可能會涉及到內部函式訪問外部函式或者全域性的變數,複習下Pyhon變數作用域,為學習閉包語法掃清障礙:

  1. 內部函式無法修改外部函式的值:因為內部函式的變數作用域不在外部
def outter():
    x = 1
    def inner():
        x = 2
        print('x in inner: %d' % x)
    print('x in outter before call inner: %d' % x)
    inner()
    print('x in outter after call inner: %d' % x)
o = outter()

結果

x in outter before call inner: 1
x in inner: 2
x in outter after call inner: 1

  1. 閉包中的訪問外部變數:nonlocal或者global
    nonlocal 語句會去搜尋本地變數與全域性變數之間的變數,其會優先尋找層級關係與閉包作用域最近的外部變數。
attr1 = 1
attr2 = 11
def outfunc():
    attr1 = 2
    attr2 = 22
    def inFunc():
        nonlocal attr1
        global attr2
        attr1 += 10
        attr2 += 100
        print('inner attr1: %d' %  attr1)
        print('inner attr2: %d' %  attr2)
    print('outter attr1 before call inFunc: %d' % attr1)
    print('outter attr2 before call inFunc: %d' % attr2)
    inFunc()
    print('outter attr1 after call outFunc: %d' % attr1)
    print('outter attr2 after call outFunc: %d' % attr2)

test = outfunc()

結果:

outter attr1 before call inFunc: 2
outter attr2 before call inFunc: 22
inner attr1: 12
inner attr2: 111
outter attr1 after call outFunc: 12
outter attr2 after call outFunc: 22

2. 從for迴圈開始

2.1 python for迴圈特性:沒有域的概念

先閱讀一段程式碼

flist = []

for i in range(3):
    def innerfunc(x):
        return x*i
    flist.append(innerfunc)


for f in flist:
    print(f(2))

讓我們猜猜最終的結果,應該是2乘以列表[0,1,2]的每個元素,得到[0,2,4]
執行結果:

4 4 4

加斷點分析:

  1. 第一個for迴圈的元素結果是一個列表,列表元素都是函式,生成列表後,列表中的引數都不賦值,也不進行計算

flist = [<function innerfunc at 0x0000023575C390D8>,
<function innerfunc at 0x0000023575C39168>,
<function innerfunc at 0x0000023575C2EDC8>]

可以認為是 [x * i, x * i, x * i],此時作用域中i為2, 即列表為[x* 2, x * 2, x * 2]

  1. 第二個for迴圈遍歷flist,傳入引數x的值,此時列表結果即為[2 * 2, 2 * 2, 2 * 2]

2.2 修改程式碼,讓返回的flist具有遞增相乘的結果:

flist = []

for i in range(3):
    def innerfunc(x):
        return x*i
    flist.append(innerfunc)
    # 在第一次迴圈的時候,就對flist中的函式賦值,即避免多次遍歷flist
    print(flist[i](2))

執行結果:

0
2
4

2.3 使用閉包

閉包的外部函式可以保證每次給函式列表flist新增元素時,i值已經固定

flist = []
for i in range(3):
    def makerfun(i):
        def fun(x):
            return x*i
        return fun
    flist.append(makerfun(i))

for f in flist:
    print(f(2))

加斷點分析:

  1. flist = [<function makerfun..fun at 0x000002DB3E22A168>, <function makerfun..fun at 0x000002DB3E21FDC8>, <function makerfun..fun at 0x000002DB3E21FCA8>]

flist依舊是一個函式列表:[makerfun(0), makerfun(1),makerfun(2)]
又因為列表元素是一個閉包,即[x * 0, x * 1, x * 2]
在第一個for迴圈結束後,i的值已經固定下來了

  1. 第二個for迴圈flist,傳入引數x的值,此時列表結果即為[2 * 0, 2 * 1, 2 * 2]
    執行結果:

0
2
4

體會過上面的例子後,就很好理解閉包的作用:儲存當前的執行環境

拿上面的例子來講,x始終是我們計算時要傳入的變數,i看做每次for迴圈執行的環境標識。
閉包實現了讓外部函式儲存了每次for迴圈的環境標識,當我們回頭再為flist的每個元素函式傳入變數x時,環境標識被閉包固化。

3. 玩棋盤遊戲

模擬棋盤遊戲,使用閉包,讓外部函式實時儲存出發的座標,內部函式完成棋子按照指定方向和步長移動的任務

origin = [0, 0]


def create(pos=origin):
    def go(direction, step):
        pos[0] += direction[0] * step
        pos[1] += direction[1] * step
        return pos
    return go


player = create()
print(player([1, 0], 10))
print(player([0, 1], 10))
print(player([-1, 0], 10))

看到這段程式碼是不是對閉包的語法有了更深的理解,注意對照這句話體會:

閉包持有外部函式的變數,這個變數叫做自由變數,當外部函式的宣告週期結束後,自由變數依然存在,因為它被閉包引用了,所以不會被回收

4. 閉包特性的另一種實現

閉包這個特性可以用類實現,把類變數看做是自由變數,類的例項持有類變數
但是使用閉包會比使用類佔用更少的資源,自由變數佔用記憶體的時間更短

class Animal():
    def __init__(self,animal):
        self.animal = animal
    def sound(self,voice):
        print(self.animal, ':', voice)

dog = Animal('dog')
dog.sound('wangwang')
dog.sound('wowo')
def voice(animal):
    def sound(voc):
        print(animal,':', voc)
    return sound

dog = voice('dog')
dog('wangwang')
dog('wowo')

可以看到輸出結果是完全一樣的,但顯然類的實現相對繁瑣,且這裡只是想輸出一下動物的叫聲,定義一個 Animal 類未免小題大做,而且 voice 函式在執行完畢後,其作用域就已經釋放,但 Animal 類及其例項 dog 的相應屬性卻一直貯存在記憶體中

5. 閉包實現流水線作業

(待補充)

相關文章