返回介绍

嵌套作用域举例

发布于 2024-01-29 22:24:16 字数 5165 浏览 0 评论 0 收藏 0

为了阐明上一小节的要点,让我们用一些真正的代码来说明。下面是一个嵌套函数作用域的例子。

首先,这是一段合法的Python代码。def是一个简单的可执行语句,可以出现在任意其他语句能够出现的地方,包括嵌套在另一个def之中。这里,嵌套的def在函数f1调用时运行;这个def生成了一个函数,并将其赋值给变量名f2,f2是f1的本地作用域内的一个本地变量。在此情况下,f2是一个临时函数,仅在f1内部执行的过程中存在(并且只对f1中的代码可见)。

但是,值得注意的是f2内部发生了什么。当打印变量x时,x引用了存在于函数f1整个本地作用域内的变量x的值。因为函数能够在整个def声明内获取变量名,通过LEGB查找法则,f2内的x自动映射到了f1的x。

这个嵌套作用域查找在嵌套的函数已经返回后也是有效的。例如,下面的代码定义了一个函数创建并返回了另一个函数。

在这个代码中,我们命名为f2的函数的调用动作的运行是在f1运行后发生的。f2记住了在f1中嵌套作用域中的x,尽管f1已经不处于激活状态。

工厂函数

根据要求的对象,这种行为有时也叫做闭合(closure)或者工厂函数——一个能够记住嵌套作用域的变量值的函数,尽管那个作用域或许已经不存在了。尽管类(将在第六部分介绍)是最适合用作记忆状态的,因为它们通过属性赋值让这个过程变得很明了,像这样的函数也提供了一种替代的解决方法。

例如,工厂函数有时用于需要及时生成事件处理、实时对不同情况进行反馈的程序中(例如,用户的输入是无法进行预测的)。作为例子,请看下面的这个函数:

这定义了一个外部的函数,这个函数简单地生成并返回了一个嵌套的函数,却并不调用这个内嵌的函数。如果我们调用外部的函数:

我们得到的是生成的内嵌函数的一个引用。这个内嵌函数是通过运行内嵌的def而创建的。如果现在调用从外部得到的那个函数:

它将会调用内嵌的函数。也就是说,maker函数内部的名为action的函数。这一部分最不平常用的就是,内嵌的函数记住了整数2,即maker函数内部的变量N的值,尽管在调用执行f时maker已经返回了值并退出。实际上,在本地作用域内的N被作为执行的状态信息保留了下来,我们返回其参数的平方运算。

现在,如果再调用外层的函数,将得到一个新的有不同状态信息的嵌套函数——得到了参数的三次方而不是平方,但是最初的仍像往常一样是平方。

这能够奏效,因为像这样对一个工厂函数的每次调用,都得到了自己的状态信息的集合。在我们的例子中,我们赋给名称g的函数记住了3,f记住了2,因为每个函数都有自己的状态信息由maker中的变量N保持。

这是一种相当高级的技术,除了那些拥有函数式编程背景的程序员们,以后在实际使用中也不会常常见到。另一方面,嵌套的作用域常常被lambda函数创建表达式使用(本章稍后将介绍它)——因为它们是表达式,它们几乎总是嵌套在一个def中。此外,函数嵌套通常用作装饰器(第38章将介绍)——在某些情况下,它是最为合理的编码模式。

通常来说,类是一个更好的像这样进行“记忆”的选择,因为它们让状态变得很明确。不使用类的话,全局变量、像这样的嵌套作用域引用以及默认的参数就是Python的函数能够保留状态信息的主要方法了。为了看看它们是如何实现的,第18章全面介绍了默认参数,而下一小节将介绍足够的默认参数的基础知识。

使用默认参数来保留嵌套作用域的状态

在较早版本的Python中,上一节中的代码执行会失败,因为嵌套的def与作用域没有一点关系——一个f2中的变量的引用只会搜索f2的本地作用域、全局作用域(f1函数以外)以及内置作用域。因为它将会跳过内嵌函数的作用域,从而会引发错误。为了解决这一问题,程序员一般都会将默认参数值传递给(记住)一个内嵌作用域内的对象:

这段代码会在任意版本的Python中工作,而且你也仍会在一些现存的Python代码中看到这样的例子。简而言之,出现在def头部的arg=val的语句表示参数arg在调用时没有值传入进来的时候,默认会使用值val。

通过修改了f2,x=x意味着参数x将会默认使用嵌套作用域中x的值,这是由于第二个x在Python进入内嵌的def之前是验证过的,所以它仍将引用f1中的x。实际上,默认参数记住了f1中x的值(也就是,对象88)。

上面这些都相当的复杂,而且它完全取决于默认值进行验证的时刻。实际上,嵌套作用域查找法则之所以加入到Python中就是为了让默认参数不再扮演这种角色。如今,Python自动记住了所需要的上层作用域的任意值,为了能够在内嵌的def中使用。

当然,最好的处方就是简单地避免在def中嵌套def,这会让程序更加得简单。下面的代码就是前边例子的等效性形式,这段代码就避免了使用嵌套。注意到,就像这个例子一样,在某一个函数内部就调用一个之后才定义的函数是可行的,只要第二个函数定义的运行是在第一个函数调用前就行,在def内部的代码直到这个函数运行时才会被验证。

如果使用这样的办法避免嵌套,你几乎都可以忘记Python中的嵌套作用域,除非需要编写之前讨论过的工厂函数风格的代码,至少对于def是这样。lambda对于def的嵌套是十分自然的,它常常依赖于嵌套作用域,正像下一节将会介绍的那样。

嵌套作用域和lambda

尽管对于def本身来说、嵌套作用域很少使用,但是当开始编写lambda表达式时,就要注意了。我们到第19章才会深入学习lambda,但是简短地说,它就是一个表达式,将会生成后面调用的一个新的函数,与def语句很相似。由于它是一个表达式,尽管能够使用在def中不能使用的地方,例如,在一个列表或是字典常量之中。

像def一样,lambda表达式引入了新的本地作用域。多亏了嵌套作用域查找层,lambda能够看到所有在所编写的函数中可用的变量。因此,以下的代码现在能够运行,但仅仅是因为如今能够使用嵌套作用域法则了。

参考之前对嵌套作用域的介绍,程序员需要使用默认参数从上层作用域传递值给lambda,就像为def做过的那样。例如,下面的代码对于所有版本的Python都可以工作。

由于lambda是表达式,所以它们自然而然地(或者更一般的)嵌套在了def中。因此,它们也就成为了后来在查找原则中增补嵌套函数作用域的最大受益者。在大多数情况下,给lambda函数通过默认参数传递值也就没有什么必要了。

作用域与带有循环变量的默认参数相比较

在已给出的法则中有个值得注意的特例:如果lambda或者def在函数中定义,嵌套在一个循环之中,并且嵌套的函数引用了一个上层作用域的变量,该变量被循环所改变,所有在这个循环中产生的函数将会有相同的值——在最后一次循环中完成时被引用变量的值。

例如,下面的程序试图创建一个函数的列表,其中每个函数都记住嵌套作用域中当前变量i的值。

尽管这样,这并不怎么有效:因为嵌套作用域中的变量在嵌套的函数被调用时才进行查找,所以它们实际上记住的是同样的值(在最后一次循环迭代中循环变量的值)。也就是说,我们将从列表中的每个函数得到4的平方的函数,因为i对于在每一个列表中的函数都是相同的值4。

这是在嵌套作用域的值和默认参数方面遗留的一种仍需要解释清楚的情况,而不是引用所在的嵌套作用域的值。也就是说,为了让这类代码能够工作,必须使用默认参数把当前的值传递给嵌套作用域的变量。因为默认参数是在嵌套函数创建时评估的(而不是在其稍后调用时),每一个函数记住了自己的变量i的值。

这是一种相当隐晦的情况,但是它会在实际情况中发生,特别是在生成应用于GUI一些部件的回调处理函数的代码中(例如,按钮的事件处理)。我们将会在第18章对默认参数和第19章对lambda做更详细的介绍,所以也许稍后会回来重新复习这一部分的内容[1]

任意作用域的嵌套

在结束这个话题时,应该提醒大家作用域可以做任意的嵌套,但是只有内嵌的函数(而不是类,将在第六部分介绍)会被搜索:

Python将会在所有的内嵌的def中搜索本地作用域,从内至外,在引用过函数的本地作用域之后,并在搜索模块的全局作用域之前进行这一过程。尽管如此,这种代码不可能会在实际中这样使用。在Python中,我们说过平坦要优于嵌套。如果你尽可能地少定义嵌套函数,那么你和同事的生活,都会变得更美好。

[1]在本部分下一章结尾的“函数陷阱”一节中,我们会看到一个和默认值自变量使用列表和字典这类可变对象有关的话题(例如,def f(a=[]):因为默认值是以单一对象来实现的,可变默认值会在调用过程中保留状态,而不是每次调用时都重新设定初始值。这根据你问的人是谁而定,它被视为支持状态保留的功能,或者是这门语言奇怪的瑕疵。第20章会再谈这个话题。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文