3

我之所以问,是因为有人创建了一个 lambda 列表的经典问题:

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 是通过引用传递的,而您不需要引用。

不过,只是为某物添加一个新名称,不应该只是创建另一个引用吗?

所以问题变成了当某些东西不是参考时的确切规则是什么?

考虑到整数是不可变的并且以下工作:

x = 3
y = x
x = 5
print(x, y) // outputs 5 3

可能解释了为什么添加该命名参数有效。i创建并捕获了具有相同值的本地。

现在,为什么在我们的 lambdas 的情况下被相同的i引用?我将一个 int 传递给函数,它被引用,如果我将它存储在一个变量中,它就会被复制。嗯。

基本上我正在寻找最简洁和抽象的方式来准确地记住它是如何工作的。何时引用相同的值,何时获得副本。如果它有任何通用名称并且有编程语言,那么它的工作方式也一样,那也会很有趣。

这是我目前的假设:

  1. 参数总是通过引用传递给函数。
  2. 分配给不可变类型的变量会创建一个副本。

无论如何,我都在问,只是为了确保并希望获得一些背景知识。

4

6 回答 6

4

这里的问题是你如何看待名字。

在您的第一个示例中,i是一个在每次循环迭代时分配给的变量。当您使用lambda创建函数时,您创建了一个访问名称i并返回其值的函数。这意味着随着名称的i变化,函数返回的值也会发生变化。

默认参数技巧起作用的原因是在定义函数时评估名称。这意味着默认值是名称当时指向的值,而不是名称本身i

i是一个标签。0,12是对象。在第一种情况下,程序分配0i,然后创建一个返回的函数i- 然后它使用1and执行此操作2。当函数被调用时,它会查找i(现在是2)然后返回它。

在第二个示例中,您分配0i,然后使用默认参数创建一个函数。该默认参数是通过评估获得的值i- 即对象0。这对1和重复2。当函数被调用时,它会将默认值分配给一个新变量i,该变量是函数本地的,与外部无关i

于 2013-06-03T21:59:58.953 回答
2

Python 并不完全通过引用值传递(至少,不是你想象的那样,来自像 C++ 这样的语言)。在许多其他语言(例如 C++)中,变量可以被认为是它们所持有的值的同义词。但是,在 Python 中,变量是指向内存中对象的名称。 (这是一个很好的解释(带图片!)) 因此,您可以将多个名称附加到一个对象上,这会产生有趣的效果。


考虑这些等效的程序片段:

// C++:
int x;
x = 10;  // line A
x = 20;  // line B

# Python:
x = 10 # line C 
x = 20 # line D

在 A 行之后,int10存储在内存中,例如内存地址0x1111

在 B 行之后,at 的内存0x1111被覆盖,所以0x1111现在保存 int20


然而,这个程序在 python 中的工作方式是完全不同的:

在 C 行之后,x指向某个内存,比如 ,0x2222并且存储的值0x222210

在 D 行之后,x指向一些不同的内存,比如 ,0x3333并且存储的值0x333320

最终,孤立的内存0x2222是 Python 收集的垃圾。


希望这可以帮助您了解 Python 和大多数其他语言中的变量之间的细微差别。

(我知道我没有直接回答你关于lambdas 的问题,但我认为这是在阅读这里的一个很好的解释之前拥有的很好的背景知识,比如@Lattyware's)

有关更多背景信息,请参阅此问题。

以下是一些最终的背景信息,以经常引用但有启发性的示例的形式:

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]

于 2013-06-03T22:04:07.883 回答
1
foo = []
for i in range(3):
    foo.append((lambda: i))  

这里因为所有的 lambda 都是在同一个范围内创建的,所以它们都指向同一个全局变量 variable 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迭代期间存储的值。

参数总是通过引用传递给函数?

实际上zinfoo[-1]仍然指向与上次迭代相同的对象i,所以是的值是通过引用传递的,但由于整数是不可变的,所以改变根本i不会影响zfoo[-1]

在下面的示例中,所有 lambda 都指向某个可变对象,因此修改 in 中的项目lis也会影响 in 中的函数foo

foo = []
lis = ([], [], [])
for i in lis:
    foo.append((lambda z = i: z))

lis[0].append("bar")
print foo[0]()          #prints ['bar']
i.append("foo")         # `i` still points to lis[-1]
print foo[-1]()         #prints ['foo']

分配给不可变类型的变量会创建一个副本?

永远不会复制任何值。

>>> x = 1000
>>> y = x       # x and y point to the same object, but an immutable object.

>>> x += 1      # so modifying x won't affect y at all, in fact after this step
                # x now points to some different object and y still points to 
                # the same object 1000

>>> x           #x now points to an new object, new id()
1001
>>> y           #still points to the same object, same id()
1000

>>> x = []
>>> y = x              
>>> x.append("foo") #modify an mutable object
>>> x,y             #changes can be seen in all references to the object
(['foo'], ['foo'])
于 2013-06-03T22:15:16.660 回答
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 根本不创建任何变量,因此它自己的本地范围是空的。当执行命中本地 undefinedi时,它位于封闭范围内。

i在第二个示例中,每个 lambda在参数列表中创建自己的变量:

foo = []
for i in range(3):
    foo.append((lambda i=i: i))

这实际上等价于lambda a=i: a,因为主体内部与赋值左侧i的 相同,而不是右侧的 。结果是本地范围内没有丢失,因此每个 lambda 都使用本地的值。iiii

更新:您的两个假设都不正确。

函数参数按值传递。传递的值是对对象的引用。通过引用传递将允许更改原始变量。

任何语言级对象的函数调用或赋值都不会发生隐式复制。在底层,因为这是按值传递,所以在调用函数时会复制对参数对象的引用,这在任何按值传递引用的语言中都很常见。

更新 2:函数评估的详细信息在这里:http ://docs.python.org/2/reference/expressions.html#calls 。有关名称绑定的详细信息,请参见上面的链接。

*在 CPython 中不会发生实际的线性搜索,因为可以在编译时确定要使用的正确变量。

于 2013-06-03T22:07:32.203 回答
0

答案是在闭包中创建的引用(函数在函数内部,内部函数从外部访问变量)是特殊的。这是一个实现细节,但在 CPython 中,该值是一种称为 a 的特殊对象cell,它允许更改变量的值,而无需将其重新绑定到新对象。更多信息在这里

变量在 Python 中的工作方式实际上相当简单。

  1. 所有变量都包含对对象的引用。
  2. 重新分配变量会将其指向不同的对象。
  3. 调用函数时,所有参数都按值传递(尽管传递的值是引用)。
  4. 某些类型的对象是可变的,这意味着它们可以在不更改其任何变量名所指向的内容的情况下进行更改。只有这些类型可以在传递时更改,因为这不需要更改对对象的任何引用。
  5. 值永远不会被隐式复制。绝不。
于 2013-06-03T22:00:15.027 回答
-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())

因此iin return i(or 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())

现在iinreturn i与外部范围不同i它是函数的局部变量anonymous。在循环的每次迭代中都会创建一个新函数(临时存储在外部范围变量anonymous中,然后永久存储在 的插槽中foo),因此每个函数都有自己的局部变量。

在创建每个函数时,其参数的默认值设置为i(在定义函数的范围内)的值。与变量的任何其他“读取”一样,它会提取当时变量引用的任何对象,此后与变量没有任何联系。2

因此,每个函数在i创建时都在外部作用域中获取默认值,然后在没有参数的情况下调用该函数时,该默认值将成为该i函数本地作用域中的值。每个函数都没有非本地引用,因此完全不受其外部发生的事情的影响。


1这是在“编译时”(将 Python 文件转换为字节码时)完成的,不考虑系统在运行时的样子;它几乎是在寻找源代码中的外部def块。i = ...所以局部变量实际上是静态解析的!如果该查找链一直落到模块全局范围内,那么 Python 假定i它将在代码运行时在全局范围内定义,并且i无论是否存在静态变量,都将其视为全局变量在模块范围内可见绑定i,因此您可以动态创建全局变量而不是本地变量。

2令人困惑的是,这意味着在 中lambda i=i: i,三个is 指的是一行中两个不同范围内的三个完全不同的“变量”。

最左边i的是“名称”,其中包含将用于默认值的值i,该值独立于函数的任何特定调用而存在;它几乎完全是存储在函数对象中的“成员数据”。

第二个i是在创建函数时评估的表达式,以获取默认值。所以该i=i位的行为非常像一个独立的语句,在包含表达式the_function.default_i = i的同一范围内进行评估。lambda

最后第三个i实际上函数内部的局部变量,它只存在于对匿名函数的调用中。

于 2013-06-03T23:45:20.423 回答