< 返回博客

Python中的变量(名称绑定)


代码块

Python的程序由代码块组成,模块、函数、类定义都属于代码块。代码块作为一个单元被解释器运行,代码块可以拥有独立的作用域

代码块在执行帧中被执行。 一个帧会包含某些管理信息(用于调试)并决定代码块执行完成后应前往何处以及如何继续执行。

变量

Python中的变量,本质上是内存中对象的名称绑定。能够创建绑定的语句有:等号赋值、函数传参、import、类定义、函数定义、forwithexceptas

变量作用域

作用域指的是变量能被访问到的范围,是一个范围,可分为:全局作用域、本地作用域、非本地作用域、内置作用域。全局作用域(global)指整个模块内;本地作用域(local)指当前代码块内;非本地作用域(nonlocal)指本地作用域以上,全局作用域以下的范围;内置作用域(builtin)是一个特定的作用域,内含一些内置变量。

变量按照作用域区分,共有三种:全局变量局部变量自由变量

  • 全局变量:拥有全局作用域
  • 局部变量:拥有本地作用域
  • 自由变量:作用域未确定

作用域的确定

变量的作用域并非在运行期确定的,而是在预编译期确定。在预编译时,编译器会:

  1. 扫描非全局代码块中的绑定语句,并将该语句所创建的名称作为该代码块的局部变量;
  2. 将在模块层级(全局)创建的名称绑定当做全局变量;
  3. 将代码块中使用但未定义的变量当做自由变量;
  4. 将使用globalnonlocal关键字定义的变量分别会当做对全局和非本地作用域内的绑定。

这些都在运行前决定,谨记。

命名空间

命名空间是一个字典,表示一个代码块所拥有的名称绑定,代码块每次执行时都会生成新的命名空间。可以通过globals()locals()函数分别访问全局和本地命名空间。使用globalnonlocal关键字时,会将指定的变量放到当前代码块的命名空间内。

对变量的绑定(创建)

在代码块中遇到绑定语句时,会在代码块对应的命名空间字典中添加对应的键值对,这时才完成了对变量的绑定。

对变量的引用(使用)

在运行期,对一个变量进行引用(使用)时,实际上就是要找到变量所绑定的对象。情况分为:

  • 如果是模块内的全局变量,在全局命名空间内查找名称绑定;
  • 如果是局部变量,则在当前代码块的命名空间内查找绑定;
  • 如果是自由变量,则从当前代码块的命名空间开始层层向上查找,直到找到一个绑定。

可以看出,闭包现象就是由自由变量的访问机制产生的。

此处所述只是语言表现,并非实现细节,对自由变量的引用可能并非直接在命名空间内搜索,而是在预编译期确定上层命名空间是否有对应的绑定。 例如,最常见的闭包:

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)的时候,赋值仍未执行,绑定也就未完成,因此抛出异常。这里就体现出了变量作用域的决定是在预编译期确定这一现象造成的影响。