代码块
Python的程序由代码块组成,模块、函数、类定义都属于代码块。代码块作为一个单元被解释器运行,代码块可以拥有独立的作用域。
代码块在执行帧中被执行。 一个帧会包含某些管理信息(用于调试)并决定代码块执行完成后应前往何处以及如何继续执行。
变量
Python中的变量,本质上是内存中对象的名称绑定。能够创建绑定的语句有:等号赋值、函数传参、import
、类定义、函数定义、for
、with
和except
的as
。
变量作用域
作用域指的是变量能被访问到的范围,是一个范围,可分为:全局作用域、本地作用域、非本地作用域、内置作用域。全局作用域(global
)指整个模块内;本地作用域(local
)指当前代码块内;非本地作用域(nonlocal
)指本地作用域以上,全局作用域以下的范围;内置作用域(builtin
)是一个特定的作用域,内含一些内置变量。
变量按照作用域区分,共有三种:全局变量
、局部变量
、自由变量
。
- 全局变量:拥有全局作用域
- 局部变量:拥有本地作用域
- 自由变量:作用域未确定
作用域的确定
变量的作用域并非在运行期确定的,而是在预编译期确定。在预编译时,编译器会:
- 扫描非全局代码块中的绑定语句,并将该语句所创建的名称作为该代码块的局部变量;
- 将在模块层级(全局)创建的名称绑定当做全局变量;
- 将代码块中使用但未定义的变量当做自由变量;
- 将使用
global
和nonlocal
关键字定义的变量分别会当做对全局和非本地作用域内的绑定。
这些都在运行前决定,谨记。
命名空间
命名空间是一个字典,表示一个代码块所拥有的名称绑定,代码块每次执行时都会生成新的命名空间。可以通过globals()
和locals()
函数分别访问全局和本地命名空间。使用global
和nonlocal
关键字时,会将指定的变量放到当前代码块的命名空间内。
对变量的绑定(创建)
在代码块中遇到绑定语句时,会在代码块对应的命名空间字典中添加对应的键值对,这时才完成了对变量的绑定。
对变量的引用(使用)
在运行期,对一个变量进行引用(使用)时,实际上就是要找到变量所绑定的对象。情况分为:
- 如果是模块内的全局变量,在全局命名空间内查找名称绑定;
- 如果是局部变量,则在当前代码块的命名空间内查找绑定;
- 如果是自由变量,则从当前代码块的命名空间开始层层向上查找,直到找到一个绑定。
可以看出,闭包现象就是由自由变量的访问机制产生的。
此处所述只是语言表现,并非实现细节,对自由变量的引用可能并非直接在命名空间内搜索,而是在预编译期确定上层命名空间是否有对应的绑定。 例如,最常见的闭包:
def outer():
a = 10
def inner():
print(a)
return inner
print(outer().__closure__) # (<cell at 0x7f379c12c828: int object at 0x7f379ca7b600>,)
可以看出,内部函数对a
的访问会被直接绑定到inner
对象上,而不会去在命名空间重新查找。
实例
请看如下代码:
a = 10 # global
def foo():
print(a)
foo() # 10, no error
没有问题,因为a属于自由变量,会向上查找。
如果变成这样:
a = 10 # global
def foo():
a = 5
print(a)
foo() # 5, no error
此时,a
在预编译期被确定为局部变量,因此在foo
代码块中隐藏了全局的a
。
但是,如果变成这样:
a = 10 # global
def foo():
print(a)
a = 5
foo() # UnboundLocalError: local variable 'a' referenced before assignment
出错了!正是因为foo
中有对a
的绑定(赋值语句),a
被当做局部变量,但是在第三行print(a)
的时候,赋值仍未执行,绑定也就未完成,因此抛出异常。这里就体现出了变量作用域的决定是在预编译期确定
这一现象造成的影响。