python面試題之“該死的for迴圈系列”(二)

格瑞姆瑞坡發表於2019-02-16

似乎只要一沾上for迴圈,難度立刻加倍,下面我們來看一道python的面試題:

要求寫出下面程式碼的輸出結果並且解釋原因。

def multipliers():
    return [lambda x:i*x for i in range(4)]
print([m(2) for m in multipliers()])

這道題涉及的知識點包括以下幾個方面:

1、列表推導式
2、匿名函式
3、閉包函式
4、for迴圈對函式的迭代呼叫
5、閉包函式的呼叫

首先我們來說一下列表推導式,只有深入理解列表推導式,我們才能理解下面這句話到底幹了些什麼事情[lambda x:i*x for i in range(4)]
引用官方文件中對於列表推導式的一個例子:squares = [x2 for x in range(10)] 這個列表推導式返回的結果為[0, 1, 4, 9, 16, 25, 36, 49, 64, 81],for迴圈通過對range(10)進行迭代後得到每個x的值,然後對它進執行x2的操作,最終結果為一個列表
那麼如果不用列表推導式如何達到這個目的呢?答案如下,這個列表推導式等同於下面的程式碼:

squares = []
for x in range(10):
    squares.append(x**2)

這段程式碼執行後,squares的結果一樣是[0, 1, 4, 9, 16, 25, 36, 49, 64, 81],根據這個例子我們可以簡單地認為列表推導式是這樣工作的:首先它會定義一個空列表,然後根據設定的條件得到一個一個的元素,同時把元素新增進列表中。

現在回到我們這道題,來看一下本題中的[lambda x:i*x for i in range(4)]這個列表推導式,如果把它拆開來的話它等價於下面的這段程式碼:

squares = []
for i in range(4):
    res = lambda x:i*x
    squares.append(res)

最終squares就是列表推導式的結果(一個列表),然後我們再研究下這個列表中的元素都是什麼。
到這裡,如果你明白了,我們就可以繼續進行下一步了——理解匿名函式。

匿名函式的關鍵字為lambda,表現形式為:lambda 引數 : 返回值,lambda後面的引數就是函式的形參,冒號後面的表示式就是返回值。
比如:lambda a, b: a+b 這個簡單的匿名函式可以傳入兩個引數a和b,結果返回a+b,這裡要記住,只有呼叫這個匿名函式,它才會執行冒號後面的程式碼,這也是函式的執行法則,只有被呼叫時,函式內部的名稱空間才會生效,在被呼叫之前它就是一個函式名指向的記憶體地址而已。

匿名函式雖然是匿名的,但是它也可以有名字,也可以作為一個結果賦值給任意的變數,所以它顯然可以成為一個函式的返回值,也可以變成一個列表的元素,只不過此時這個列表的元素是匿名函式對應的記憶體地址罷了。見下面的例子:

#匿名函式直接賦值給變數lam
lam = lambda a,b:a+b
#此時lam指向了匿名函式的記憶體地址
print(lam)#此時的lam就是一個記憶體地址:<function <lambda> at 0x7fecdc6b7e18>
res = lam(2,5) #呼叫匿名函式,把結果賦值給res
print(res)
<function <lambda> at 0x7fecdc6b7e18>
7

接下來我們說一下閉包,當前函式引用到上一層函式的區域性名稱空間的變數時就會觸發閉包規則。我們說觸發了閉包的函式叫做閉包函式,但是要注意一點:只有當呼叫閉包函式的時候它才會去引用外層函式的變數,因為在呼叫閉包函式之前,閉包內部的名稱空間還不存在。

然後我們回頭看這道題的程式碼:

def multipliers():
    return [lambda x:i*x for i in range(4)]
print([m(2) for m in multipliers()])
#根據前面的敘述,我們可以把它改成容易理解的形式:
def multipliers():
    squares = []
    for i in range(4):
        res = lambda x:i*x
        squares.append(res)
    return squares
print([m(2) for m in multipliers()])

匿名函式lambda x:i*x引用了外層函式multipliers()的名稱空間內的變數i,所以它觸發了閉包規則,然後函式multipliers()的返回值是一個列表,這個列表的元素為四個閉包函式名指向的記憶體地址,雖然for i in range(4)這段程式碼裡面的i的值分別被賦予了 0 1 2 3這四個值,但是閉包函式res並沒有引用這四個值,因為閉包函式此時此刻還沒有被真正呼叫,列表推導式僅僅是把四個匿名函式指向的記憶體地址儲存在了一個列表裡,因為沒有呼叫,所以匿名函式內部的程式碼並沒有執行,也就不存在引用。
所以函式multipliers()的返回值就是這樣的一個列表:[lambda x:ix,lambda x:ix,lambda x:ix,lambda x:ix]

我們來看最後一條語句print([m(2) for m in multipliers()])
for m in multipliers() 這條語句到底幹了什麼?其實它乾的事情只有一個,那就是遍歷了函式multipliers()返回的列表,在遍歷列表的同時把每個匿名函式賦值給了m,把它拆分來看就是這樣:
m = lambda x:i*x
m = lambda x:i*x
m = lambda x:i*x
m = lambda x:i*x
並且每次都執行了一次 m(2),也就是每次都呼叫了一下匿名函式,注意:此時此刻匿名函式才真正被呼叫了,然後它會引用外層名稱空間的變數i,那麼此時i的值是多少呢?
因為for i in range(4)這個for迴圈已經執行完畢,i的值等於3,所以每次當執行m(2)時,i的值都等於3
所以每次呼叫m(2)的結果都是6
最終輸出結果為[6, 6, 6, 6]

def multipliers():
    return [lambda x:i*x for i in range(4)]
print([m(2) for m in multipliers()])
[6, 6, 6, 6]

把這道面試題中的所有列表推導式拆開的話 它應該是下面這個樣子,結果完全一樣:

def multipliers():
    squares = []
    for i in range(4):
        res = lambda x:i*x
        squares.append(res)
    return squares

#print(multipliers()),此時此刻如果我們列印一下這個函式,也就是呼叫一下看看返回結果,你會發現,它就是一個由四個函式記憶體地址組成的列表:
```[<function multipliers.<locals>.<lambda> at 0x7fecdc6de2f0>, 
 <function multipliers.<locals>.<lambda> at 0x7fecdc6de510>, 
 <function multipliers.<locals>.<lambda> at 0x7fecdc6de158>, 
 <function multipliers.<locals>.<lambda> at 0x7fecdc6de268>]```

squares2 = []
for m in multipliers():
    squares2.append(m(2))
print(squares2)
[6, 6, 6, 6]

相關文章