嵌套函数中的局部变量

2024-11-21 08:34:00
admin
原创
5
摘要:问题描述:好的,请耐心听我说完,我知道这看起来非常复杂,但请帮助我理解发生了什么。from functools import partial class Cage(object): def __init__(self, animal): self.animal = animal d...

问题描述:

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

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:

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

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

当您实际调用该函数时,该闭包将用于查看调用该函数时cage周围范围内的值。问题就在这里。当您调用函数时,该函数已经完成了其结果的计算。在执行期间的某个时刻,局部变量被分配了、和字符串中的每一个,但在函数的末尾,包含最后一个值。因此,当您调用每个动态返回的函数时,您将获得打印的值。get_petters`cage'cow''dog''cat'cage'cat''cat'`

解决方法是不依赖闭包。你可以改用部分函数,​​创建新的函数作用域,或将变量绑定为关键字参数的默认值

  • 部分函数示例,使用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)))
  • 创建新的范围示例:

def scoped_cage(cage=None):
    def pet_function():
        print "Mary pets the " + cage.animal + "."
    return pet_function

yield (animal, partial(gotimes, scoped_cage(cage)))
  • 将变量绑定为关键字参数的默认值:

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

yield (animal, partial(gotimes, pet_function))

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

解决方案 2:

我的理解是,当实际调用产生的 pet_function 时,会在父函数命名空间中寻找 holder ,而不是之前。

所以当你这样做

funs = list(get_petters())

您生成 3 个函数来查找最后创建的笼子。

如果你将最后一个循环替换为:

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

您实际上将获得:

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

解决方案 3:

这源于以下原因

for i in range(2): 
    pass

print(i)  # prints 1

迭代之后,的值i被延迟存储为其最终值。

作为生成器,该函数可以工作(即依次打印每个值),但是当转换为列表时,它会在生成器上运行cage,因此对( ) 的所有调用cage.animal都会返回 cats。

解决方案 4:

让我们简化这个问题。定义:

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这些函数所绑定的 s:

>>> 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对象引用的是animal,它是在get_petters()调用时创建的。但是,在生成器函数运行时,它引用的对象animal会发生变化。str

在第一个循环中,在每次迭代期间,我们都会创建所有的fs,但我们只在生成器get_petters()完全用尽并且list已经创建了函数后才调用它们。

在第二个循环中,每次迭代期间,我们都会暂停get_petters()生成器并在每次暂停后调用f。因此,我们最终会检索animal生成器函数暂停时的值。

正如@Claudiu 在回答类似问题时所说:

创建了三个独立的函数,但它们各自都具有定义环境的闭包 - 在本例中为全局环境(如果循环位于另一个函数内,则为外部函数的环境)。然而,这正是问题所在 - 在这个环境中,animal是变异的,并且所有闭包都引用相同的animal

[编者注:i已更改为animal。]

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用