什么时候在Python中引入新名称?

时间:2013-06-03 21:51:56

标签: python reference scope immutability

我问的是因为有人创建了一个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。

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

这是我目前的假设:

  1. 参数总是通过引用传递给函数。
  2. 分配给不可变类型的变量会创建一个副本。
  3. 无论如何,我要问的是,只是为了确保并希望得到一些背景。

6 个答案:

答案 0 :(得分:4)

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

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

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

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

在第二个示例中,您将0分配给i,然后使用默认参数创建一个函数。该默认参数是通过评估i得到的值 - 即对象0。对12重复此操作。调用函数时,它会将该默认值分配给函数本地且与外部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指向某个记忆,例如0x22220x2222处存储的值为10

在D行之后,x指向一些不同的内存,例如0x33330x3333中存储的值为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中的工作方式实际上非常简单。

  1. 所有变量都包含对象的引用。
  2. 重新分配变量会将其指向另一个对象。
  3. 调用函数时,所有参数都按值传递(尽管传递的值是引用)。
  4. 某些类型的对象是可变的,这意味着可以更改它们而无需更改其任何变量名称所指向的内容。传递时只能更改这些类型,因为这不需要更改对该对象的任何引用。
  5. 永远不会隐式复制值。从不。

答案 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 实际上是函数内的局部变量,它只存在于对匿名函数的调用中。