嵌套函数中的局部变量

116

好的,请耐心听我讲,我知道这看起来非常复杂,但请帮助我理解发生了什么。

from functools import partial

class Cage(object):
    def __init__(self, animal):
        self.animal = animal

def gotimes(do_the_petting):
    do_the_petting()

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        cage = Cage(animal)

        def pet_function():
            print "Mary pets the " + cage.animal + "."

        yield (animal, partial(gotimes, pet_function))

funs = list(get_petters())

for name, f in funs:
    print name + ":", 
    f()

输出结果:

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

基本上,为什么我没有得到三种不同的动物?cage 不是“打包”进嵌套函数的局部作用域中吗?如果不是,那么调用嵌套函数如何查找局部变量?

我知道遇到这种问题通常意味着做错了,但我想了解发生了什么。


1
尝试使用for animal in ['cat', 'dog', 'cow']... 我相信总会有人来解释这个问题 - 这是 Python 中的一个陷阱 :) - Jon Clements
4个回答

122

嵌套函数在执行时查找父级作用域中的变量,而不是在定义时。

函数体被编译,并验证“自由”变量(未通过赋值在函数本身中定义),然后将其绑定为闭包单元格到函数中,代码使用索引引用每个单元格。pet_function因此有一个自由变量(cage),然后通过闭包单元格引用它,索引为0。闭包本身指向get_petters函数中的局部变量cage

当您实际调用函数时,该闭包将用于查看调用函数时周围范围内的值。这就是问题所在。在调用函数时,函数已经完成计算结果。在执行期间,局部变量的某个点被分配了<‘cow’>、<‘dog’>和<‘cat’>字符串,但在函数结束时包含最后一个值<‘cat’>。因此,当您调用每个动态返回的函数时,会打印出值<‘cat’>。
解决方法是不要依赖闭包。您可以使用部分函数,创建新的函数范围,或将变量绑定为关键字参数的默认值。
  • Partial function example, using functools.partial():

    from functools import partial
    
    def pet_function(cage=None):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, partial(pet_function, cage=cage)))
    
  • Creating a new scope example:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
    
  • Binding the variable as a default value for a keyword parameter:

    def pet_function(cage=cage):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, pet_function))
    

在循环中无需定义scoped_cage函数,编译只会在循环外进行一次,而不是在每次迭代中进行。


1
今天我在工作脚本上撞了3个小时的墙。你最后提到的一点非常重要,也是我遇到这个问题的主要原因。我的代码中有很多带有闭包的回调函数,但是在循环中尝试相同的技术却让我陷入了困境。 - DrEsperanto

12

我的理解是,当调用yielded pet_function时,Python会在父函数命名空间中查找cage,而不是在之前。

因此,当你执行

funs = list(get_petters())

你生成了三个函数,它们将找到最新创建的笼子。

如果你用以下代码替换你最后的循环:

for name, f in get_petters():
    print name + ":", 
    f()

你将实际获得:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

7
这源于以下原因。
for i in range(2): 
    pass

print(i)  # prints 1

在迭代完i的值后,它会被懒惰地存储为最终值。
作为一个生成器函数,它将工作(即逐个打印每个值),但是在转换为列表时,它会遍历生成器,因此所有对 cage.animal 的调用都会返回猫。

1

让我们简化问题。定义:

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        def pet_function():
            return "Mary pets the " + animal + "."

        yield (animal, pet_function)

然后,就像问题中一样,我们得到:

>>> for name, f in list(get_petters()):
...     print(name + ":", f())

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

但是如果我们避免先创建一个list()
>>> for name, f in get_petters():
...     print(name + ":", f())

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

发生了什么?为什么这个微小的差别会完全改变我们的结果?


如果我们查看list(get_petters()),从不断变化的内存地址可以清楚地看出,我们确实产生了三个不同的函数:
>>> list(get_petters())

[('cow', <function get_petters.<locals>.pet_function at 0x7ff2b988d790>),
 ('dog', <function get_petters.<locals>.pet_function at 0x7ff2c18f51f0>),
 ('cat', <function get_petters.<locals>.pet_function at 0x7ff2c14a9f70>)]

然而,看一下这些函数所绑定的 cell
>>> for _, f in list(get_petters()):
...     print(f(), f.__closure__)

Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)

>>> for _, f in get_petters():
...     print(f(), f.__closure__)

Mary pets the cow. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a95670>,)
Mary pets the dog. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a952f0>,)
Mary pets the cat. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c3f437f0>,)

对于两个循环,cell 对象在迭代过程中保持不变。然而,预期的是,在第二个循环中它所引用的特定 str 会发生变化。cell 对象指的是在调用 get_petters() 时创建的 animal。然而,animal 会随着生成器函数的运行而改变其所引用的 str 对象。
在第一个循环中,每次迭代期间我们都创建了所有的 f,但只有在生成器 get_petters() 完全耗尽并已经创建了一个函数列表后才会调用它们。
在第二个循环中,每次迭代期间,我们都会暂停 get_petters() 生成器并在每次暂停后调用 f。因此,我们最终检索到的是生成器函数暂停时刻的 animal 值。
正如 @Claudiu 在 类似问题 的回答中所说:
创建了三个单独的函数,但它们都具有闭包环境 - 在本例中为全局环境(或者如果循环放置在另一个函数内,则为外部函数的环境)。然而这恰恰是问题所在——在该环境中,animal被改变,所有的闭包都引用同一个animal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接