关于python:带有循环的Scope变量

Scope Variable with a Loop

我读了Python官方手册中的一句话。

If a lambda or def defined within a function is nested inside a loop, and the >nested function references an enclosing scope variable that is changed by that >loop, all functions generated within the loop will have the same value—the value >the referenced variable had in the last loop iteration. In such cases, you must >still use defaults to save the variable’s current value instead.

至于剧本:

1
2
3
4
5
6
7
def makeActions():
     acts=[]
     for i in range(5):
         acts.append(lambda x: i**x)
     return acts
acts=makeActions()
print(acts)

我知道这个问题的样子和解决方法。但我就是不明白原因。请看一下我的解释:

1
2
3
4
5
6
7
8
9
10
def makeActions():
    acts=[]
    for i in range(5):
        acts.append(lambda x: i**x) #step2-step6: each lambda would be stored
                                    #in the list, however, with its own"i"
    return acts

acts=makeActions() #step1: when the function is called, the list would be
                   #created. And lambda would be called as well.
print(acts)

我的观点是,在每次迭代中,即使没有给出参数x,也会调用每个lambda。因此,在列表"acts"中,每个元素(即每个lambda)具有不同的"i"。

我知道我错了,但你能告诉我为什么吗?谢谢!


为了更清楚地说明这个主题,在lambda中对i的引用并没有改变—您可以看到i的每次迭代都会生成一个新的对象:

1
2
3
4
5
6
7
8
9
def makeActions():
  acts=[]
  for i in range(5):
    print('i:', i, 'object id:', id(i))
    acts.append(lambda: id(i))
  print('final i:', i, 'object id:', id(i))
  return acts
acts=makeActions()
print([fn() for fn in acts])

退换商品

1
2
3
4
5
6
7
i: 0 object id: 140418781832928
i: 1 object id: 140418781832960
i: 2 object id: 140418781832992
i: 3 object id: 140418781833024
i: 4 object id: 140418781833056
final i: 4 object id: 140418781833056
[140418781833056, 140418781833056, 140418781833056, 140418781833056, 140418781833056]

来自i的引用总是指向它的最后一个分配对象,因此,i的ID在最后一次修改后不会改变。

如果要在创建lambda时保留该值,则需要将引用的值"trap"到所需的位置和时间,例如,可以将lambda创建委托给函数:

1
2
3
4
5
6
7
8
9
10
11
12
def delegated(d):
  return lambda: id(d)

def makeDelegatedActions():
  acts=[]
  for i in range(5):
    print('i:', i, 'object id:', id(i))
    acts.append(delegated(i))
  print('final i:', i, 'object id:', id(i))
  return acts
acts=makeDelegatedActions()
print([fn() for fn in acts])

哪些回报

1
2
3
4
5
6
7
i: 0 object id: 140418781832928
i: 1 object id: 140418781832960
i: 2 object id: 140418781832992
i: 3 object id: 140418781833024
i: 4 object id: 140418781833056
final i: 4 object id: 140418781833056
[140418781832928, 140418781832960, 140418781832992, 140418781833024, 140418781833056]

这里是在线演示


每个lambda只知道从其封闭范围(即makeActions调用)中获取i。他们在被称为自己之前不会这样做。i是同一范围内的同一名称,在循环过程中恰好取不同的值,循环结束后只保存最后一个值。所以lambda最终创建了5个相同的函数。

可以工作的变体:

1
2
3
4
5
6
def expgen(n):
    def myexp(x):
        return n**x
    return myexp

acts = [expgen(i) for i in range(5)]

在这种情况下,从中提取n的范围是expgen的范围,它在列表理解中有不同的调用,并且每个函数都将独立工作。