python面试题之“该死的for循环系列”(二)
似乎只要一沾上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]