作为 Python 面试官,每次面试中我几乎都会和候选人聊起 作用域 以及 名字空间 等基本概念。但就算这么基础的内容,也有不少人没有完全掌握,也因此与工作机会失之交臂。
|
|
以这个程序为例,代码中出现的每个变量的作用域分别是什么?程序中总共涉及几个名字空间? Python 以怎样的顺序查找一个变量呢?为了解答这些问题,需要对 Python 变量的作用域以及名字空间有准确的认识。
名字绑定
赋值
在 Python 中,变量只是一个与实际对象绑定起来的名字,变量定义本质上就是建立名字与对象的约束关系。因此,赋值语句本质上就是建立这样的约束关系,将右边的对象与左边的名字绑定在一起:
|
|
我经常在面试中问:除了赋值语句,还有哪些语句可以完成名字绑定?能准确回答的候选人寥寥无几。实际上,除了赋值语句外, Python 中还有好几类语句均与名字绑定相关,我们接着一一介绍。
模块导入
我们导入模块时,也会在当前上下文创建一个名字,并与被导入对象绑定:
|
|
函数、类定义
我们定义函数/类时,本质上是创建了一个函数/类对象,然后将其与函数/类名绑定:
|
|
as关键字
除此之外, as
关键字也可以在当前上下文建立名字约束关系:
|
|
以上这几类语句均可在当前上下文建立名字约束,有着与赋值语句类似的行为,因此可以看作是 广义的赋值语句 。
作用域
现在问题来了,一个名字引入后,它的可见范围有多大呢?
我们以一个面试真题开始讨论:以下例子中 3 个 print 语句分别输出什么?
|
|
例子中,第1行引入的名字 a 对整个模块都可见,第4行和第10行均可访问到它,因此这两个地方输出 1
;而第7行引入的名字 a 却只有函数 f2 内部可以访问到,第8行优先访问内部定义的 a ,因此这里将输出 2
。
由此可见,在不同的代码区域引入的名字,其影响范围是不一样的。第 1 行定义的 a 可以影响到 f1
,而 f2
中定义的 a
却不能。再者,一个名字可能在多个代码区域中定义,但最终只能使用其中一个。
一个名字能够施加影响的程序正文区域,便是该名字的 作用域 。在 Python
中,一个名字在程序中某个区域能否起作用,是由名字引入的位置决定的,而不是运行时动态决定的。因此, Python
具有 静态作用域 ,也称为 词法作用域 。那么,程序的作用域是如何划分的呢?
Python
在编译时,根据语法规则将代码划分为不同的 代码块 ,每个代码块形成一个 作用域 。首先,整个 .py
文件构成最顶层的作用域,这就是 全局作用域 ,也称为 模块作用域 ;其次,当代码遇到 函数定义 ,函数体成为当前作用域的 子作用域 ;再次,当代码遇到 类定义 ,类定义体成为当前作用域的子作用域。
一个名字在某个作用域引入后,它的影响范围就被限制在该作用域内。其中,全局作用域对所有直接或间接内嵌于其中的子作用域可见;函数作用域对其直接子作用域可见,并且可以传递。
按照这个划分方式,真题中的代码总共有3个作用域: A
为最外层作用域,即全局作用域; f1
函数体形成作用域 B
,是 A
的子作用域; f2
函数体又形成作用域 C
,也是 A
的子作用域。
作用域 A
定义的变量 a
对于对 A
及其子作用域 B
、 C
可见,因此 f1
也可以访问到。理论上, f2
也可以访问到 A
中的 a
,只不过其作用域 C
也定义了一个 a
,优先访问本作用域内的。 C
作用域内定义的任何名字,对 A
和 B
均不可见。
A B C 三个作用域嵌套关系如左下所示,访问关系如右下所示:
箭头表示访问关系,例如作用域 B 中的语句可以访问到作用域 A 中的名字,反过来则不行。
闭包作用域
这个例子借助闭包实现提示信息定制功能:
|
|
根据前面介绍的规则,我们对代码进行作用域划分,结果如下:
A B C 三个作用域嵌套关系如左下所示,访问关系如右下所示:
毫无疑问, B C 均在全局作用域 A 内,因此都可以访问到 A 中的名字。由于 B 是函数作用域,对其子作用域 C 可见。因此, hint 属于 B
作用域,而位于 C 作用域的语句可以访问它,也就不奇怪了。
类作用域
我们接着以一个简单的类为例,考察类作用域:
|
|
根据前面介绍的规则,我们对代码进行作用域划分,结果如下:
其中, B 对应着类定义体,暂且叫做类作用域。各个作用域嵌套关系以及访问关系分别如下:
同样,全局作用域 A 对其他所有内嵌于其中的作用域可见。因此,函数 yelp 作用域 D 可以访问到全局作用域 A 中的名字 slogan 。但是由于 B 不是函数作用域,对其子作用域不可见。因此, yelp_group 函数作用域 F 访问不到类作用域 B 中的名字 group ,而 group 在全局作用域 A 中位定义,第 17 行便抛异常了。
在 Python 中,类可以动态创建,甚至在函数中返回。在函数中创建并返回类,可以按函数参数对类进行动态定制,有时很有用。那么,这种场景中的作用域又该如何划分呢?点击 阅读原文,获取更多详细内容!
【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: