Python新手坑 | lambda、全域性變數與區域性變數、作用域、柯里化
從一個看似簡單的問題引入
首先我們來看這樣一個例子,假設你正試圖編寫一個函式,呼叫時可以返回0~4的平方,你選擇用for loop 和 lambda 來實現:
squares = []
for x in range(5):
squares.append(lambda: x**2)
根據計劃,你的函式使用起來應該是這個樣子的:
>>>squares[2]()
>4
>>>squares[4]()
>16
實際上它卻是這個樣子的:
>>>squares[2]()
>16
>>>squares[4]( )
>16
事實上,不論輸入0、1、2、3、4,最後都會得到16。為什麼?要想解決這個疑惑,我們需要看看在這段程式碼執行的每一步中,究竟發生了什麼。
發生了什麼?
想要直觀地認識程式碼執行時的具體步驟,這裡不得不強烈推薦一個十分強大且實用的開源工具——Python Tutor。開啟網頁輸入程式碼後,只要點選Visualize Execution
,即可看到程式碼逐步執行的過程。
輸入上文中的程式碼,執行後得到如下的結果:
問題出在哪兒?
從結果上看,lambda
只記住了x
的最後一個值4
。這是由於x
不是lambda
的區域性變數(local variable),而是一個全域性變數(global variable)
lambda
的作用域(scope) 中。只有當lambda
被呼叫時,x
的值才會被傳給它,而不是在定義的時候就已經傳給函式,這種特性與python的惰性計算(lazy evaluation) 有關。迴圈結束後,x
的值已經確定為4
,此時再呼叫lambda
,就只有4
這個值被傳輸進函式。
如何解決?
問題的原因已經確定,那麼如何解決也就不困難了。直接針對作用域這個關鍵點,做細節上的調整——給lambda
建立一個屬於它的區域性變數即可,直接上程式碼:
squares = []
for x in range(5):
squares.append(lambda n=x: n**2)
可以看到,程式碼中增加了一個區域性變數n,我們再用一次
圖中黃色區域顯示,每次定義函式時,都被傳入了局部變數n的值,最終也確實得到了我們最初想要的結果。
以一個稍有區別的例子結束
看到這裡,不妨做一道測試題,如果能夠答對,說明對上文講解的知識已經初步理解了。閱讀下面的程式碼,嘗試回答能夠得到怎樣的執行結果:
f_list = [lambda x: x**i for i in range(5)]
[f_list[j](10) for j in range(5)]
如果你的答案是:
[10000, 10000, 10000, 10000]
那麼恭喜你,答對了!
如果不是上面這個答案,而是:
[10, 100, 1000, 10000]
那你最好向上翻,複習一下前面講解的內容。
其實,這與前文的例子區別並不大,只不過把for loop
換成了list comprehension
。
注意,這裡的i既不是全域性變數,也不是區域性變數,而是介於兩者之間的
nonlocal
變數,這個作用域的引入也是python3相較於python2的改進之一。
其實,列表推導式與lambda
聯用的功能是很強大的,尤其是在資料分析時,以後還有機會講到。
接下來,怎樣修改程式碼才能得到第二種輸出結果呢?這裡我們採用一個與之前不同的方法:
f_list = [(lambda y: lambda x: x**y)(i) for i in range(5)]
[f_list[j](10) for j in range(5)]
這樣的方法稱為柯里化(currying)。
初步理解Currying
建立函式時,常常會遇到需要多個引數的情況,而currying則是定義需要一個引數的函式,並把這個函式作為新的引數帶入下一個函式中。以此類推,最後的函式相當於直接建立一個多引數的函式。
這裡是一個小例子:
currying_sum = lambda x: lambda y: x+y
currying_plus_one = currying_sum(1)
currying_sum
接收一個引數1
,並且將自身作為一個新的引數傳給currying_plus_one
,該函式可以將輸入引數加1並返回。