python面試題之「該死的for循環系列」(二)

彷佛只要一沾上for循環,難度馬上加倍,下面咱們來看一道python的面試題:

要求寫出下面代碼的輸出結果而且解釋緣由。python

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

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

一、列表推導式
二、匿名函數
三、閉包函數
四、for循環對函數的迭代調用
五、閉包函數的調用

首先咱們來講一下列表推導式,只有深刻理解列表推導式,咱們才能理解下面這句話到底幹了些什麼事情[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],根據這個例子咱們能夠簡單地認爲列表推導式是這樣工做的:首先它會定義一個空列表,而後根據設定的條件獲得一個一個的元素,同時把元素添加進列表中。app

如今回到咱們這道題,來看一下本題中的[lambda x:i*x for i in range(4)]這個列表推導式,若是把它拆開來的話它等價於下面的這段代碼:函數

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

最終squares就是列表推導式的結果(一個列表),而後咱們再研究下這個列表中的元素都是什麼。
到這裏,若是你明白了,咱們就能夠繼續進行下一步了——理解匿名函數。code

匿名函數的關鍵字爲lambda,表現形式爲:lambda 參數 : 返回值,lambda後面的參數就是函數的形參,冒號後面的表達式就是返回值。
好比:lambda a, b: a+b 這個簡單的匿名函數能夠傳入兩個參數a和b,結果返回a+b,這裏要記住,只有調用這個匿名函數,它纔會執行冒號後面的代碼,這也是函數的執行法則,只有被調用時,函數內部的命名空間纔會生效,在被調用以前它就是一個函數名指向的內存地址而已。ip

匿名函數雖然是匿名的,可是它也能夠有名字,也能夠做爲一個結果賦值給任意的變量,因此它顯然能夠成爲一個函數的返回值,也能夠變成一個列表的元素,只不過此時這個列表的元素是匿名函數對應的內存地址罷了。見下面的例子:內存

#匿名函數直接賦值給變量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

接下來咱們說一下閉包,當前函數引用到上一層函數的局部命名空間的變量時就會觸發閉包規則。咱們說觸發了閉包的函數叫作閉包函數,可是要注意一點:只有當調用閉包函數的時候它纔會去引用外層函數的變量,由於在調用閉包函數以前,閉包內部的命名空間還不存在。文檔

而後咱們回頭看這道題的代碼:io

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]
相關文章
相關標籤/搜索