在廖雪峯的官網上看到一個很有意思題目。關於閉包的,有興趣的朋友可以看一下這裏, 做一下這個題目,當然需要一點閉包的知識。下面我簡述一下:
利用閉包返回一個計數器函數,每次調用它返回遞增整數。
# 修改下面這個函數
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上看看大佬們的回答。很有學習的價值。
寫文不易, 還請大家多多支持!
參考資料: