1. 程式人生 > >Python新手坑 | lambda、全域性變數與區域性變數、作用域、柯里化

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,即可看到程式碼逐步執行的過程。
輸入上文中的程式碼,執行後得到如下的結果:
圖1

問題出在哪兒?

從結果上看,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,我們再用一次

Python Tutor看看具體的執行過程與之前有什麼區別:
圖2圖中黃色區域顯示,每次定義函式時,都被傳入了局部變數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並返回。

References