我问的是因为有人创建了一个lambdas列表的经典问题:
foo = []
for i in range(3):
foo.append((lambda: i))
for l in foo:
print(l())
并且意外地仅获得两个输出。
通常提出的解决方案是使i
成为这样的命名参数:
foo = []
for i in range(3):
foo.append((lambda i=i: i))
for l in foo:
print(l())
它产生了0, 1, 2
的所需输出,但现在发生了一些神奇的事情。它有点像预期的那样,因为Python是通过引用传递的,你不需要引用。
但是,只是在某个地方添加一个新名称,这不应该只是创建另一个引用吗?
所以问题变成什么是什么时候什么不是参考?
考虑到int是不可变的,以下是有效的:
x = 3
y = x
x = 5
print(x, y) // outputs 5 3
可能解释了添加命名参数的原因。
创建并捕获了具有相同值的本地i
。
现在为什么,在我们的lambdas的情况下引用相同的i
?我将一个int传递给函数并重新启用它,如果我将它存储在一个变量中,它就会被复制。 HM。
基本上我正在寻找最简洁和抽象的方式来记住这是如何工作的。何时引用相同的值,何时获取副本。如果它有任何通用名称,并且有编程语言,它的工作方式也同样有趣。
这是我目前的假设:
无论如何,我要问的是,只是为了确保并希望得到一些背景。
答案 0 :(得分:4)
这里的问题是你如何看待名字。
在第一个示例中,i
是每次循环迭代时分配的变量。当您使用lambda
创建函数时,您创建一个访问名称i
并返回其值的函数。这意味着名称i
发生变化时,函数返回的值也会发生变化。
默认参数技巧的作用是在定义函数时评估名称。这意味着默认值是当时i
名称指向的值,而不是名称本身。
i
是一个标签。 0
,1
和2
是对象。在第一种情况下,程序会将0
分配给i
,然后创建一个返回i
的函数 - 然后使用1
和2
执行此操作。调用该函数时,它会查找i
(现在为2
),然后将其返回。
在第二个示例中,您将0
分配给i
,然后使用默认参数创建一个函数。该默认参数是通过评估i
得到的值 - 即对象0
。对1
和2
重复此操作。调用函数时,它会将该默认值分配给函数本地且与外部i
无关的新变量i
。
答案 1 :(得分:1)
Python没有完全通过引用或按值传递(至少,不是你想象它的方式,来自像C ++这样的语言)。 在许多其他语言(例如C ++)中,变量可以被认为是它们所持有的值的同义词。 但是,在Python中,变量是指向内存中对象的名称。 (This is a good explanation (with pictures!)) 因此,您可以将多个名称附加到一个对象,这可能会产生有趣的效果。
考虑这些等效的程序片段:
// C++:
int x;
x = 10; // line A
x = 20; // line B
和
# Python:
x = 10 # line C
x = 20 # line D
在A行之后,int 10
存储在内存中,例如存储在内存地址0x1111
。
在第B行之后,0x1111
的内存被覆盖,因此0x1111
现在拥有int 20
然而,这个程序在python中的工作方式却截然不同:
在第C行之后,x
指向某个记忆,例如0x2222
,0x2222
处存储的值为10
在D行之后,x
指向一些不同的内存,例如0x3333
,0x3333
中存储的值为20
< / p>
最终,0x2222
处的孤立内存被Python垃圾收集。
希望这有助于您掌握Python和大多数其他语言中变量之间的细微差别。
(我知道我没有直接回答你关于lambda
的问题,但我认为这是一个很好的背景知识,在阅读这里的一个很好的解释之前,比如@ Lattyware's)
有关更多背景信息,请参阅this question。
以下是一些最终的背景信息,以经常引用但有指导性的例子的形式出现:
print 'Example 1: Expected:'
x = 3
y = x
x = 2
print 'x =', x
print 'y =', y
print 'Example 2: Surprising:'
x = [3]
y = x
x[0] = 2
print 'x =', x
print 'y =', y
print 'Example 3: Same logic as in Example 1:'
x = [3]
y = x
x = [2]
print 'x =', x
print 'y =', y
输出结果为:
示例1:预期:
x = 2
y = 3
示例2:令人惊讶:
x = [2]
y = [2]
示例3:与示例1中的逻辑相同:
x = [2]
y = [3]
答案 2 :(得分:1)
出现lambda问题列表,因为两个片段中引用的i
是同一个变量。
只有两个不同的变量存在于两个不同的范围内时,它们才存在。请参阅以下链接了解何时发生这种情况,但基本上任何新函数(包括lambda)或类都会建立自己的范围,模块也是如此,而其他任何东西都没有。请参阅:http://docs.python.org/2/reference/executionmodel.html#naming-and-binding
但是,在读取变量的值时,如果未在当前本地范围中定义,则会搜索封闭的本地范围*。你的第一个例子就是这种行为:
foo = []
for i in range(3):
foo.append((lambda: i))
for l in foo:
print(l())
每个lambda根本不创建变量,因此它自己的局部范围是空的。当执行命中本地未定义的i
时,它位于封闭范围内。
在第二个示例中,每个lambda在参数列表中创建自己的i
变量:
foo = []
for i in range(3):
foo.append((lambda i=i: i))
这实际上等同于lambda a=i: a
,因为正文中的i
与作业左侧的i
相同,而不是{{1} }} 在右手侧。结果是本地范围中没有遗漏i
,因此每个lambda使用本地i
的值。
更新:您的两个假设都不正确。
函数参数按值传递。传递的值是对象的引用。传递引用将允许更改原始变量。
在任何语言级对象的函数调用或赋值中都不会发生隐式复制。在引擎盖下,因为它是按值传递,所以在调用函数时会复制对参数对象的引用,这在通过值传递引用的任何语言中都是如此。
更新2:功能评估的详细信息如下:http://docs.python.org/2/reference/expressions.html#calls。有关名称绑定的详细信息,请参阅上面的链接。
i
在CPython中没有实际的线性搜索,因为可以在编译时确定要使用的正确变量。
答案 3 :(得分:1)
foo = []
for i in range(3):
foo.append((lambda: i))
这里因为所有lambda都是在相同的范围内创建的,所以它们都指向相同的全局变量变量i
。所以,当实际调用它们时,将返回任何值i
。
foo = []
for i in range(3):
foo.append((lambda z = i: id(z)))
print id(i) #165618436
print(foo[-1]()) #165618436
在每个循环中,我们将i
的值赋给局部变量z
,因为在解析函数时会计算默认参数,因此值z
只是指向值在迭代期间由i
存储。
参数总是通过引用传递给函数吗?
实际上z
中的foo[-1]
仍然指向与上一次迭代的i
相同的对象,因此yes值是通过引用传递的,但是整数是不可变的,因此更改{{ 1}}根本不会影响i
的{{1}}。
在下面的示例中,所有lambda都指向一些可变对象,因此修改z
中的项目也会影响foo[-1]
中的函数:
lis
分配给不可变类型的变量会创建一个副本吗?
永远不会复制任何值。
foo
答案 4 :(得分:0)
答案是在闭包中创建的引用(其中函数在函数内部,而内部函数从外部函数访问变量)是特殊的。这是一个实现细节,但在CPython中,值是一种称为cell
的特定对象,它允许更改变量的值而不将其重新绑定到新对象。 More info here
变量在Python中的工作方式实际上非常简单。
答案 5 :(得分:-2)
这种行为与传递参数的方式实际上没什么关系(总是以相同的方式;在Python中没有区别,有时候事物通过引用传递,有时候通过值传递)。相反,问题在于如何找到名称本身。
lambda: i
创建一个当然等同于:
的函数def anonymous():
return i
i
是一个名称,在anonymous
的范围内。但它永远不会在该范围内(甚至不作为参数)。因此,为了表示任何i
必须是来自某个外部范围的名称。要查找合适的名称i
,Python将查看在源代码中定义anonymous
的范围(然后从那里开始类似),直到找到i
的定义 1
所以这个循环:
foo = []
for i in range(3):
foo.append((lambda: i))
for l in foo:
print(l())
几乎就像你写的那样:
foo = []
for i in range(3):
def anonymous():
return i
foo.append(anonymous)
for l in foo:
print(l())
因此i
(或return i
)中的lambda: i
最终与外部作用域(即循环变量)中的i
相同。并不是说它们都是对同一个对象的引用,而是它们都是同一个名称。因此,foo
中存储的函数根本不可能返回不同的值;他们都返回了一个名称所引用的对象。
要证明这一点,请观看在循环后删除变量i
时会发生什么:
>>> foo = []
>>> for i in range(3):
foo.append((lambda: i))
>>> del i
>>> for l in foo:
print(l())
Traceback (most recent call last):
File "<pyshell#7>", line 2, in <module>
print(l())
File "<pyshell#3>", line 2, in <lambda>
foo.append((lambda: i))
NameError: global name 'i' is not defined
你可以看到问题并不是每个函数都有一个本地i
绑定到错误的东西,而是每个函数都返回相同全局变量的值 ,我现在已经删除了。
OTOH,当你的循环看起来像这样:
foo = []
for i in range(3):
foo.append((lambda i=i: i))
for l in foo:
print(l())
这很像:
foo = []
for i in range(3):
def anonymous(i=i):
return i
foo.append(anonymous)
for l in foo:
print(l())
现在i
中的return i
不与外部范围相同i
;它是函数anonymous
的局部变量。在循环的每次迭代中创建一个新函数(临时存储在外部作用域变量anonymous
中,然后永久存储在foo
的一个插槽中),因此每个函数都有自己的局部变量。
在创建每个函数时,其参数的默认值设置为i
的值(在定义函数的范围内)。就像变量的任何其他“读取”一样,它会拉出当时变量引用的任何对象,然后与变量没有任何关联。 2
因此,每个函数都会获得默认值i
,因为它在创建时位于外部作用域中,然后在没有参数的情况下调用函数时,默认值将成为{{的值1}}在该函数的本地范围内。每个函数都没有非本地引用,因此完全不受外部事件的影响。
1 这是在“编译时”(当Python文件转换为字节码时)完成的,不考虑系统在运行时的含义;它几乎在字面上寻找源代码中带有i
的外部def
块。所以局部变量实际上是静态解决的!如果该查找链一直落到模块全局范围,那么Python假定i = ...
将在代码运行时在全局范围内定义,并且只将i
视为一个全局变量,无论模块范围内是否存在i
的静态可见绑定,因此您可以动态创建全局变量而不是本地变量。
2 令人困惑的是,这意味着在i
中,三个lambda i=i: i
在一行的两个不同范围内引用了三个完全不同的“变量”。
最左边的i
是“name”,其中包含将用于默认值i
的值,该值独立于函数的任何特定调用而存在;它几乎完全是存储在函数对象中的“成员数据”。
第二个i
是在创建函数时计算的表达式,以获取默认值。因此i
位的行为非常类似于独立语句i=i
,在包含the_function.default_i = i
表达式的同一范围内进行评估。
最后,第三个lambda
实际上是函数内的局部变量,它只存在于对匿名函数的调用中。