Python閉包與nonlocal

在廖雪峯的官網上看到一個很有意思題目。關於閉包的,有興趣的朋友可以看一下這裏, 做一下這個題目,當然需要一點閉包的知識。下面我簡述一下:

利用閉包返回一個計數器函數,每次調用它返回遞增整數。

# 修改下面這個函數
def createCounter():
   def counter():
       pass
   return counter
# 測試:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
    print('測試通過!')
else:
    print('測試失敗!')

方法一

說實話這題對我來說還是有點難度的,但我嘗試了幾次之後也找到一個比較track的方法。一開始我是這麼寫的。

def createCounter():
    i = 0
    def counter(i=i):
        i = i+1
        return i
    return counter
# 執行結果是: 1 1 1 1 1

這樣當然是錯的, 因爲整數 是 不可變對象,當你作爲參數傳進去時都會創建一個新的內存空間,這裏邊其實還有很多學問,不是很瞭解的可以看一下stackoverflow上的這個回答。雖然失敗了,但也讓我想到一個track的方法,就是把i換成可變對象

def createCounter():
    i = [0]
    def counter():
        i[0] = i[0]+1
        return i[0]
    return counter
# 執行結果是: 1 2 3 4 5

OK, 這樣就沒有問題了。但這並不是一個好的解決方法, 利用可變對象的這個特性有可能會引起變量作用域混亂的。於是我又想到了另一種解決。

方法二

另一種方法就是使用generator,在createCounter函數下創建一個從1開始的整數generator, 然後在cuonter函數中調用。由於generator保存的是算法,當調用next函數時就可以計算出下一個的值,直到沒有元素報錯。當然這裏不用擔心,generator可以創建無限集合。

def createCounter():
    def inter():
        n = 1
        while True:
            yield n
            n = n+1 
    f = inter()
    def counter():
        return next(f)
    return counter

上面的代碼中,inter()就是一個包含從1開始的所有整數的generator。然後在counter裏邊調用。每次計算下一個的值。這樣就可以實現計數的功能。說到generator,stackoverflow上有一個回答值得一讀,即使你已經掌握這個也可以讀一下,這個回答應該還是python問答當中排名第一的。鏈接在這裏

方法三

emmmm,想到這兩種方法已經是極限了,於是我往評論區翻了翻,看一下大佬們有什麼做法。然後就看到一個我沒見過的關鍵字...其中有一個大佬是這麼做

def creat_counter():
    i=0
    def counter():
        nonlocal i
        i=i+1
        return i
    return counter

學了python這麼久,第一次看到nonlocal這個關鍵字,果然我還是太菜了。。。
不過從語句上看nonlocal的作用應該是把i變成全局變量,這樣每次修改都可以生效,跟global關鍵字有點像。既然找到一個知識盲點,那就將它徹底解決吧。

nonlocal 與 global

說了這麼多,是時候回到主題了,nonlocal關鍵字到底是什麼?在什麼情況下用呢?簡單來說,nonlocal關鍵字是用來改變變量的作用域的,直接解釋不太好懂。先來看兩個例子吧。

def outside():
    msg = "Outside!"
    def inside():
        msg = "Inside!"
        print(msg)
    inside()
    print(msg)

執行結果是什麼呢?

Inside!
Outside!

結果應該很好理解, 在outside函數裏面定義了insede函數並且執行。當運行outside函數時,inside裏面的msg變量指向了"Inside!",outside裏面的msg指向了"Outside!", 也就是說這裏其實有兩個msg變量,並且指向了不同的值。如下圖所示:


變量示意圖

再來看下面這個例子:

def outside():
    msg = "Outside!"
    def inside():
        nonlocal msg
        msg = "Inside!"
        print(msg)
    inside()
    print(msg)

現在的執行結果就變成了:

Inside!
Inside!

兩段代碼之間的差別僅在於下面的例子多了一句 nonlocal msg。這裏的nonlocal關鍵字起到了什麼作用呢?nonlocal意思是告訴python,不要重新創建msg變量,而是使用outside中的msg變量來賦值。畫個圖就很好懂了。

變量示意圖

在這個例子中, msg變量只被創建了一次,首先將"Outside!"賦值給msg,然後將"Inside!"賦值給了msg, 此時的msg已經指向了"Inside!"。因此執行的結果兩個都是"Inside!"。

現在我們知道了,nonlocal是用來改變變量的作用域的。本例中,nonlocal將inside函數裏面的msg變量的作用域變成了outside塊中的區域。nonlocal跟global 這兩個關鍵字非常像,不同之處在於nonlocal用於外部函數作用域的變量,而global用於全局範圍內的變量。

這就是nonlocal關鍵字的作用。但是還有一點值得注意,先看下面的例子。

def outside():
    d = {"outside": 1}
    def inside():
        d["inside"] = 2
        print(d)
    inside()
    print(d)

大家覺得輸出是什麼呢?
實際輸出是這樣的:

{'outside': 1, 'inside': 2}
{'outside': 1, 'inside': 2}

原因嘛,當然是因爲dict是可變對象了,但由於 d["inside"] = 2 這樣的語句是有點讓人迷惑的,看起來很像重新賦值。實際上dict的賦值是調用了setitem方法。這樣看就不會感到迷惑了。

# 下面的代碼是等價的。
d["inside"] = 2
d.__setitem__("inside", 2)

關於nonlocal關鍵字,應該講清楚了吧。至於python的閉包,其實還是挺複雜的,上面的只是幾個例子,想要更加深入的學習可以上stackoverflow上看看大佬們的回答。很有學習的價值。

寫文不易, 還請大家多多支持!

參考資料:

  1. https://www.smallsurething.com/a-quick-guide-to-nonlocal-in-python-3/
  2. https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014317799226173f45ce40636141b6abc8424e12b5fb27000
  3. https://taizilongxu.gitbooks.io/stackoverflow-about-python/content/1/README.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章