11.8 变量作用域

标识符的作用域是定义为其声明在程序里的可应用范围,或者即是我们所说的变量可见性。换句话说,就好像在问你自己,你可以在程序里的哪些部分去访问一个制定的标识符。变量可以是局部域或者全局域。

11.8.1 全局变量与局部变量

定义在函数内的变量有局部作用域,在一个模块中最高级别的变量有全局作用域。在编译器理论里著名的“龙书”中,阿霍、塞西和乌尔曼作了如下总结:

“声明适用的程序的范围被称为了声明的作用域。在一个过程中,如果名字在过程的声明之内,它的出现即为过程的局部变量;否则的话,出现即为非局部。”

全局变量的一个特征是除非被删除掉,否则它们的存活到脚本运行结束,且对于所有的函数,他们的值都是可以被访问的,然而局部变量,就像它们存放的栈,暂时地存在,仅仅只依赖于定义它们的函数现阶段是否处于活动。当一个函数调用出现时,其局部变量就进入声明它们的作用域。在那一刻,一个新的局部变量名为那个对象创建了,一旦函数完成,框架被释放,变量将会离开作用域。

11.8 变量作用域 - 图1

上面的例子中,global_str是全局变量,而local_str是局部变量。foo()函数可以对全局和局部变量进行访问,而代码的主体部分只能访问全局变量。

11.8 变量作用域 - 图2核心笔记:搜索标识符(也称变量,名字,等等)

当搜索一个标识符的时候,Python先从局部作用域开始搜索。如果在局部作用域内没有找到那个名字,那么就一定会在全局域找到这个变量否则就会被抛出NameError异常。一个变量的作用域和它寄住的名称空间相关。我们会在第12章正式介绍名称空间;对于现在只能说子空间仅仅是将名字映射到对象的命名领域,现在使用的变量名字虚拟集合。作用域的概念和用于找到变量的名称空间搜索顺序相关。当一个函数执行的时候,所有在局部命名空间的名字都在局部作用域内。那就是当查找一个变量的时候,第一个被搜索的名称空间。如果没有在那找到变量的话,那么就可能找到同名的全局变量。这些变量存储(搜索)在一个全局及内建的名称空间。

仅仅通过创建一个局部变量来“隐藏”或者覆盖一个全局变量是有可能的。回想一下,局部名称空间是首先被搜索的,存在于其局部作用域。如果找到一个名字,搜索就不会继续去寻找一个全局域的变量,所以在全局或者内建的名称空间内,可以覆盖任何匹配的名字。

同样,当使用全局变量同名的局部变量的时候要小心。如果在赋予局部变量值之前,你在函数中(为了访问这个全局变量)使用了这样的名字,你将会得到一个异常(NAMEERROR或者Unbound-LocalError),而这取决于你使用的Python版本。

11.8.2 globa语句

如果将全局变量的名字声明在一个函数体内的时候,全局变量的名字能被局部变量给覆盖掉。这里有另外的例子,与第一个相似,但是该变量的全局和局部的特性就不是那么清晰了。

11.8 变量作用域 - 图3

得到如下输出:

11.8 变量作用域 - 图4

我们局部的bar将全局的bar推出了局部作用域。为了明确地引用一个已命名的全局变量,必须使用global语句。global的语法如下:

11.8 变量作用域 - 图5

修改上面的例子,可以更新我们代码,这样我们便可以用全局版本的is_this_global而无须创建一个新的局部变量。

11.8 变量作用域 - 图6

11.8.3 作用域的数字

Python从语法上支持多个函数嵌套级别,就如在Python2. 1中的,匹配静态嵌套的作用域。然而,在2. 1之前的版本中,最多为两个作用域:一个函数的局部作用域和全局作用域。虽然存在多个函数的嵌套,但你不能访问超过两个作用域。

11.8 变量作用域 - 图7

虽然这代码在今天能完美的运行…

11.8 变量作用域 - 图8

在Python2.1之前执行它将会产生错误。

11.8 变量作用域 - 图9

在函数bar()内访问foo()的局部变量m是非法的,因为m是声明为foo()的局部变量。从bar()中可访问唯一的作用域为局部作用域和全局作用域。foo()的局部作用域没有包含在上面两个作用域的列表中。注意‘print m’语句的输出成功了,而对bar()的函数调用却失败了。幸运的是,由于Python的现有嵌套作用语规则,今天就不存在这个问题了。

11.8.4 闭包

由于Python的静态嵌套域,如我们早先看到的,定义内部函数变得很有用处。在下面的部分中,我们将着重讨论作用域和lambda,但是在Python 2. 1之前,当作用域规改则变为今天这样之前,内部函数也会遭受到相同的问题。如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。定义在外部函数内的但由内部函数引用或者使用的变量被称为自由变量。闭包在函数式编程中是一个重要的概念,Scheme和Haskell便是函数式编程中两种。闭包从语法上看很简单(和内部函数一样简单)但是仍然很有威力。

闭包将内部函数自己的代码和作用域以及外部函数的作用结合起来。闭包的词法变量不属于全局名称空间域或者局部的——而属于其他的名称空间,带着“流浪”的作用域。(注意这不同于对象因为那些变量是存活在一个对象的名称空间但是闭包变量存活在一个函数的名称空间和作用域)那么为什么你会想要用闭包?

闭包对于安装计算、隐藏状态和在函数对象和作用域中随意地切换是很有用的。闭包在GUI或者在很多API支持回调函数的事件驱动编程中是很有些用处的。以绝对相同的方式,应用于获取数据库行和处理数据。回调就是函数。闭包也是函数,但是他们能携带一些额外的作用域。它们仅仅是带了额外特征的函数……另外的作用域。

你可能会觉得闭包的使用和这章先前介绍的偏函数应用非常的相似,但是与闭包的使用相比,PFA更像是currying,因为闭包和函数调用没多少相关,而是关于使用定义在其他作用域的变量。

1. 简单的闭包的例子

下面是使用闭包简单的例子。我们会模拟一个计数器,同样也通过将整型包裹为一个列表的单一元素来模拟使整型易变。

11.8 变量作用域 - 图10

counter()做的唯一一件事就是接受一个初始化的值来开始计数,并将该值赋给列表count唯一一个成员。然后定义一个incr()的内部函数。通过在内部使用变量count,我们创建了一个闭包,因为它现在携带了整个counter()作用域。incr()增加了正在运行的count然后返回它。然后最后的魔法就是counter()返回一个incr,一个(可调用的)函数对象。如我们交互地运行这个函数,将得到如下的输出——注意这看起来和实例化一个counter对象并执行这个实例有多么相似:

11.8 变量作用域 - 图11

11.8 变量作用域 - 图12

有点不同的是我们能够做些原来需要我们写一个类做的事,并且不仅仅是要写,还必需覆盖掉这个类的call_()特别方法来使他的实例可调用。这里我们能够使用一对函数来做这件事。

现在,在很多情况下,类是最适合使用的。闭包更适合需要一个必需有自己的作用域的回调函数情况,尤其是回调函数是很小巧而且简单的,通常也很聪明。跟平常一样,如果你使用了闭包,对你的代码进行注释或者用文档字符串来解释你正做的事是很不错的主意。

2. 追踪闭包词法的变量

下面两个部分包含了给高级读者的材料……如果你愿意的话,你可以跳过去。我们将讨论如何能使用函数的func_closure属性来追踪自由变量。这里有个显示追踪的代码片段。

如果我们运行这段代码,将得到如下输入:

11.8 变量作用域 - 图13

这个例子说明了如何能通过使用函数的func closure属性来追踪闭包变量。

11.8 变量作用域 - 图14

11.8 变量作用域 - 图15

3. 逐行解释

1 ~ 4行

这段脚本由创建模板来输出一个变量开始:它的名字、ID和值,然后设置变量w、x、y和z。我们定义了模板,这样便不需要多次拷贝相同输出格式的字符串。

6 ~ 9、26 ~ 31行

fl()函数的定义包括创建一个局部变量x、y和z,以及一个内部函数f2()的定义。(注意所有的局部变量遮蔽或者隐藏了对他们同名的全局变量的访问)。如果f2()使用了任何的定义在fl()作用域的变量,比如说,非全局的和非f2()的局部域的,那么它们便是自由变量,将会被fl.func_closure追踪到。

9 ~ 10、19 ~ 24行

这几行实际上是对fl()的拷贝,对f2()做相同的事,定义了局部变量y和z,以及对一个内部函数f3()。此外,这里的局部变量会遮蔽全局以及那些在中间局部化作用域的变量,如fl() 。如果对于f3()有任何的自由变量,他们会在这里显示出来。

毫无疑问,你会注意到对自由变量的引用是存储在单元对象里,或者简单地说,单元。这些东西是什么呢?单元是在作用域结束后使自由变量的引用存活的一种基础方法。

举例来说,我们假设函数f3()已经被传入到其他一些函数,这样便可在稍后,甚至是f2()完成之后调用它。你不想要让f2()的栈出现,因为即使我们仅仅在乎f3()使用的自由变量,栈也会让所有的f2()’s的变量保持存活。单元维持住自由变量以便f2()的剩余部分能被释放掉。

12 ~ 17行

这个部分描绘了f3()的定义,创建一个局部的变量z。接着显示w、x、y、z,这4个变量从最内部作用域逐步向外的追踪到的。在f3() 、f2()或fl()中都是找不到变量w的,所以这是个全局变量。在f3()或者f2()中,找不到变量x,所以来自fl()的闭包变量。相似地,y是一个来自f2()的闭包变量。最后,z是f3()的局部变量。

33 ~ 38行

main()中剩余的部分尝试去显示fl()的闭包变量,但是什么都不会发生因为在全局域和fl()的作用域之间没有任何的作用域——没有fl()可以借用的作用域,因此不会创建闭包——所以第34行的条件表达式永远不会求得True。这里的这段代码仅仅是有修饰的目的。

4. *高级闭包和装饰器的例子

回到11. 3. 6部分,我们看到了一个使用闭包和装饰器的简单例子,deco.py。接下来就是稍微高级点的例子,来给你演示闭包的真正的威力。应用程序“logs”函数调用。用户选择是要在函数调用之前或者之后,把函数调用写入日志。如果选择贴日志,执行时间也会显示出来。

这个例子演示了带参数的装饰器,该参数最终决定哪一个闭包会被用的。这也是闭包的威力的特征。

11.8 变量作用域 - 图16

如果执行这个脚本,你将会得到和下面相似的输出:

11.8 变量作用域 - 图17

5. 逐行解释

5 ~ 10、28 ~ 32行

这段代码描绘了logged()函数的核心部分,其职责就是获得关于何时函数调用应该被写入日志的用户请求。它应该在目标函数被调用前还是之后呢?logged()有3个在它的定义体之内的助手内部函数:log(), pre_logged()和post_logged()。 log()是实际上做日志写入的函数。它仅仅是显示标准输出函数的名字和参数。如果你愿意在“真实的世界中”使用该函数的话,你很有可能会把输出写到一个文件、数据库或者标准错误(sys.stderr) 。 logged()在28〜32行的最后的部分实际上是函数中非函数声明的最开始的代码。读取用户的选择然后返回*logged()函数中的一个便能用目标函调用并包裹它。

12 ~ 26行

pre_logged()和post_logged()都会包装目标函数然后根据它的名字写入日志,比如,当目标函数已经执行之后,post_loggeed()会将函数调用写入日志,而pre_logged()则是在执行之前。

根据用户的选择,pre_logged()和post_logged()其中之一会被返回。当这个装饰器被调用的时候,首先对装饰器和其参数进行求值,比如logged(时间)。然后返回的函数对象作为目标的函数的参数进行调用,比如,pre_logged (f)或者post_logged (f)。

两个*logged()函数都包括了一个名为wrapper()的闭包。当合适将其写入日志的时候,它便会调用目标函数。这个函数返回了包裹好的函数对象,该对象随后将被重新赋值给原始的目标函数标识符。

34 ~ 38行

这段脚本的主要部分简单地装饰了hello()函数并将用修改过的函数对象一起执行它。当你在38行调用hello()的时候,它和你在35行创建的函数对象已经不是一回事了。34行的装饰器用特殊的装饰将原始函数对象进行了包裹并返回这个包裹后的hello()版本。

11.8.5 作用域和lambda

Python的lambda匿名函数遵循和标准函数一样的作用域规则。一个lambda表达式定义了新的作用域,就像函数定义,所以这个作用域除了局部lambda函数,对于程序其他部分,该作用域都是不能对进行访问的。

那些声明为函数局部变量的lambda表达式在这个函数体内是可以访问的;然而,在lambda语句中的表达式有和函数相同的作用域。你也可以认为函数和一个lambda表达式是同胞。

11.8 变量作用域 - 图18

现在知道这段代码能很好的运行。

11.8 变量作用域 - 图19

……然而,我们必须在回顾下过去,去看下原来的python版本中让代码运行必需的,一种极其普遍的做法。在2. 1之前,我们将会得到一个错误,如同你在下面看到的一样,因为函数和lambda都可访问全局变量,但两者都不能访问彼此的局部作用域。

11.8 变量作用域 - 图20

在上面的例子中,虽然lambda表达式在foo()的局部作用域中创建,但他仅仅只能访问两个作用域:它自己的局部作用域和全局的作用域(同样见11. 8. 3小节)。解决的方法是加入一个变量作为默认参数,这样我们便能从外面的局部作用域传递一个变量到内部。在我们上面的例子中,我们将lambda的那一行修改成这样:

11.8 变量作用域 - 图21

由于这个改变,程序能运行了。外部y的值会作为一个参数传入,成为局部的y (lambda函数的局部变量)。你可以在所有你遇到的Python代码中看到这种普遍的做法;然而,这不表明存在改变外部y值的可能性,比如:

11.8 变量作用域 - 图22

输出“完全错误”:

11.8 变量作用域 - 图23

原因是外部y的值被传入并在lambda中“设置”,所以虽然其值在稍后改变了,但是lambda的定义没有变。那时唯一替代的方案就是在lambda表达式中加入对函数局部变量y进行引用的局部变量z.

11.8 变量作用域 - 图24

为了获得正确的输出所有的一切都是必需的:

11.8 变量作用域 - 图25

这同样也不可取因为现在所有调用bar()的地方都必需改为传入一个变量。从Python2.1开始,在没有任何修改的情况下整个程序都完美的运行。

11.8 变量作用域 - 图26

正确的静态嵌套域(最后)被加入到Python中,你会不高兴吗?许多老前辈一定不会。你可以在PEP 227中阅读到更多关于这个重要改变的信息。

11.8.6 变量作用域和名称空间

从我们在这章的学习中,我们可以看见任何时候,总有一个或者两个活动的作用域——不多也不少。我们要么在只能访问全局作用域的模块的最高级,要么在一个我们能访问函数局部作用域和全局作用域的函数体内执行。名称空间是怎么和作用域关联的呢?

从11. 8. 1小节的核心笔记中,我们也可以发现,在任何给定的时间,存在两个或者三个的活动的名称空间。从函数内部,局部作用域包围了局部名称空间,第一个搜寻名字的地方。如果名字存在的话,那么将跳过检查全局作用域(全局和内建的名称空间)。

我们现在将给出例子11. 9,一个到处混合了作用域的脚本。我们将确定此程序输出作为练习留给读者。

局部变量隐藏了全局变量,正如在这个变量作用程序中显示的。程序的输出会是什么(以及为什么)呢?

11.8 变量作用域 - 图27

  1. 3.1 小节有更多关于名称空间和变量作用域的信息。