"学习Python,第四版." 提到:
稍后调用嵌套函数时会查找封闭的范围变量.
但是,我认为当一个函数退出时,它的所有本地引用都会消失.
def makeActions():
acts = []
for i in range(5): # Tries to remember each i
acts.append(lambda x: i ** x) # All remember same last i!
return acts
Run Code Online (Sandbox Code Playgroud)
makeActions()[n]每个人都是一样的,n因为变量i在调用时以某种方式查找.Python如何查找此变量?难道它根本不存在因为makeActions已经退出了吗?为什么Python不执行代码直观建议,并通过在循环运行时用for循环中的当前值替换i来定义每个函数?
从评论到THC4k:
我认为我错误地认为Python在内存中构建函数的方式.我在想,当遇到a makeActions()[n]或者a时n,Python会生成与该函数相关的所有必要的机器指令,并将其保存在内存中的某处.现在我认为它更像是将函数保存为文本字符串(并将其与闭包所需的引用捆绑在一起),并在每次调用函数时重新解析它.
我认为这是很明显的,当你认为会发生什么i的名字不是某种价值.你的lambda函数确实像"拍X:查找我的价值,我计算**X" ......所以,当你实际运行的功能,查找i 只是那么这样i的4.
您也可以使用当前数字,但必须使Python将其绑定到另一个名称:
def makeActions():
def make_lambda( j ):
return lambda x: j * x # the j here is still a name, but now it wont change anymore
acts = []
for i in range(5):
# now you're pushing the current i as a value to another scope and
# bind it there, under a new name
acts.append(make_lambda(i))
return acts
Run Code Online (Sandbox Code Playgroud)
它可能看起来令人困惑,因为你经常被教导变量和它的值是相同的 - 这是真的,但仅限于实际使用变量的语言.Python没有变量,而是名称.
关于你的评论,实际上我可以更好地说明这一点:
i = 5
myList = [i, i, i]
i = 6
print(myList) # myList is still [5, 5, 5].
Run Code Online (Sandbox Code Playgroud)
你说你把我改为6,这不是实际发生的事情:i=6意思是"我有一个价值,6我想说出它i".您已经用作i名称的事实对Python没有任何意义,它只会重新分配名称,而不是更改它的值(仅适用于变量).
你可以说,当前指向的myList = [i, i, i]任何值i(数字5)都有三个新名称:mylist[0], mylist[1], mylist[2].这与调用函数时发生的情况相同:参数被赋予新名称.但这可能违反任何关于列表的直觉......
这可以解释行为的例子:你分配mylist[0]=5,mylist[1]=5,mylist[2]=5-难怪他们不要当你重新分配的变化i.如果i是可静态的东西,例如列表,那么更改i也会反映所有条目myList,因为您只有相同值的不同名称!
您可以mylist[0]在左侧使用的简单事实=证明它确实是一个名称.我喜欢叫=的指定名称操作:它需要一个名字上的左边,右边一个表达式,然后计算表达式(通话功能,查找值名称后面),直到它有一个值,最后给出了名字价值.它没有改变任何东西.
好吧,引用(和指针)只有在我们有某种可寻址内存时才有意义.这些值存储在内存中的某个位置,引用会引导您到达该位置.使用引用意味着在内存中访问该位置并使用它执行某些操作.问题是Python 没有使用这些概念!
Python VM没有内存概念 - 值浮动在空间的某个地方,名称是连接到它们的小标签(通过一个小的红色字符串).名称和值存在于不同的世界中!
编译函数时,这会产生很大的不同.如果您有引用,则您知道所引用对象的内存位置.然后你可以简单地用这个位置替换then引用.另一方面,名称没有位置,因此您必须执行的操作(在运行时)遵循那个小红色字符串并使用另一端的任何内容.这就是Python编译函数的方式:代码中有一个名称,它会添加一条指令来确定该名称的含义.
所以基本上Python完全编译函数,但是名称在嵌套命名空间中被编译为查找,而不是某种对内存的引用.
当您使用名称时,Python编译器将尝试确定它所属的命名空间的位置.这会导致从它找到的命名空间加载该名称的指令.
这让你回到原来的问题:在lambda x:x**i,在i编译器中编译为查找makeActions(因为i在那里使用).Python不知道,也不关心它背后的价值(它甚至不必是一个有效的名称).代码运行的代码i在其原始命名空间中查找并提供或多或少的预期值.
创建闭包时会发生什么:
for块.i只要for循环正在运行,该帧中的值就会不断变化 - 每个赋值都会i更新该i帧中的绑定.i不再更新值.i会在调用时获取父框架中的任何值.因为在for循环中你创建了闭包,但实际上并没有调用它们,所以调用后的值i将是它完成所有循环后的最后一个值.makeActions将创建不同的帧.i在这种情况下,您不会重用for循环的前一帧,也不会更新前一帧的值.简而言之:框架像其他Python对象一样被垃圾收集,在这种情况下,额外的引用保持在与for块对应的框架周围,因此当for循环超出范围时它不会被破坏.
要获得所需的效果,您需要为i要捕获的每个值创建一个新帧,并且需要使用对该新帧的引用来创建每个lambda.你不会从for块本身那里得到它,但是你可以通过调用辅助函数来获得它,这将建立新的框架.请参阅THC4k的答案,了解这些方面的一种可能的解决方案.