Python名称空间与作用域
名称空间
在 垃圾回收机制 这篇文章中我们曾讲过,在内存中变量名和变量值分别存储在 栈区 与 堆区 中。现在我们又有了一个概念叫 名称空间(Name space),其实名称空间就是将栈区内的名字划分了几个区域。
当我们在Python中定义一个变量、函数或类时,它们都被存储在特定的名称空间中。这些名称可能包括关键字、函数、类、模块、变量等。名称空间是从名称到对象的映射,大部分的名称空间都是通过 Python 字典来实现的。
名称空间提供了在项目中避免名字冲突的一种方法。各个名称空间是独立的,没有任何关系的,所以一个名称空间中不能有重名,但不同的名称空间是可以重名而没有任何影响。
我们可以举一个简单的的例子:一个文件夹(目录)中可以包含多个文件夹,每个文件夹中不能有相同的文件名,但 不同文件夹 中的文件可以重名。
Python中有 3 种名称空间:
- 内置名称空间(Built-in)
- 全局名称空间(Global)
- 局部名称空间(Local)
可以通过locals( )(获取局部name space)、globals( )(获取全局name space) 函数来获取名称空间的值(字典),在程序的不同位置执行结果不一定一致,因为结果是针对当前位置来说的。
名称空间查找顺序
- 名称空间查找顺序为以 当前位置 为起点以 局部的名称空间 -> 全局名称空间 -> 内置名称空间 的顺序查找。
- 函数名称空间的查找顺序以定义阶段为基准
如果找不到变量 a,它将放弃查找并引发一个 NameError 异常:
NameError: name 'a' is not defined。
名称空间的生命周期
名称空间的生命周期取决于对象的作用域,如果对象执行完成(比如说一个函数执行完、一个 python 文件执行完等等),则该名称空间的生命周期就结束。
内置名称空间
包含了Python内置的函数、类、对象等。这些可以在任何地方直接使用,无需导入任何模块。
示例:
print("Hello, world!") # print 是 Python 内置函数,处于内置名称空间内。
全局名称空间
在 模块(暂时理解为一个以.py结尾的 python 文件)中定义的所有变量、函数和类都在该名称空间内,它们即是全局变量,也是该模块的属性。
示例:
x = 10 # x为全局变量,位于全局名称空间内。
def my_func():
print(x)
my_func() # >> 10
根据 名称空间查找顺序 在函数内部找x,函数内部没有则去全局内找,找到x = 10就打印出了 10。
局部名称空间
主要指 函数内部 的名称空间,它们只能在函数内部可见,访问函数外部的变量需要使用global或者nonlocal关键字。
示例:
def my_func():
y = 20 # y 为局部变量,处于局部名称空间
print(y)
my_func() # >> 20
print(y) # NameError: name 'y' is not defined,因为 y 是在函数中
后面的print(y)会报错是因为,在上面 名称空间查找顺序 中我们说过,Python 名称的查找顺序为以 当前位置 为起点以 局部的名称空间 -> 全局名称空间 -> 内置名称空间 的顺序查找。当前处于全局,则在全局寻找 y ,显然没有。然后去内置的名称空间去找,显然也没有。因此就会报错。
作用域
作用域就是一个 Python 程序可以直接访问 名称空间 的区域。
在一个 python 程序中,直接访问一个变量,会 从内到外 依次访问所有的作用域直到找到,否则会报未定义的错误。变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。
Python的作用域一共有 4 种,分别是:
- L(Local):最内层,包含局部变量,比如一个函数、方法内部。
- E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。
- G(Global):当前脚本的最外层,比如当前模块的全局变量。
- B(Built-in): 包含了内建的变量、关键字等。
Enclosing:两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
访问顺序为: L –> E –> G –> B。
先在局部找,在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,最后去内置中找。
示例:
g_count = 0 # 全局作用域 -> G
def outer():
o_count = 1 # 闭包函数外的函数中 -> E
def inner():
i_count = 2 # 局部作用域 -> L
在Python中,只有模块,类以及函数才会引入新的作用域,其它的代码块(if 语句等等)是不会引入新的作用域的。不要和其他编程语言混淆。
由于作用域的限制内部作用域难以访问外部作用域,当内部作用域想修改外部作用域的变量时,Python为我们提供了global和nonlocal关键字。
global关键字
global 关键字可以声明全局变量,使得一个函数内部的变量赋值操作改为对全局变量的修改操作。
示例:
# global 实现全局变量
count = 0 # 全局变量
def increment():
global count # 使用 global 声明 count 为全局变量
count += 1 # 对全局变量 count 进行加 1 操作
print(count)
increment() # >> 1
increment() # >> 2
nonlocal关键字
nonlocal 关键字用于嵌套函数中,允许将变量绑定到 最近的非全局作用域。它通常与 闭包 一起使用,可用来在内层函数中对外层函数的变量进行修改。
示例:
# nonlocal 改变外层函数变量
def outer():
x = "hello"
def inner():
nonlocal x # 将变量 x 绑定到最近的非全局作用域,也就是 outer 函数的作用域
x = "world" # 修改了 outer 函数中的变量 x
print("inner:", x)
inner()
print("outer:", x)
outer() # >> inner: world outer: world
一些例子
为了帮助更好的理解名称空间与作用域,下面列举了一些例子。
x=3
def fun():
print(x) # 局部没有去全局找
运行结果:3
x=3
def fun():
x=2 # 定义并赋值本地变量 x
print(x) # 局部有就打印局部的
运行结果:2
x=1
def fun():
print(x)
x=2
fun()
运行结果:报错。
函数名称空间的查找顺序以定义阶段为基准,所以在上面的代码中,在定义 fun 时就确认了 fun 函数内有 x ,所以执行
print(x)时就会引用到 fun 函数中的变量 x ,但是 x 在使用(执行print(x))前没有先声明(使用前必须先声明),所以出错。
x=1
def fun1():
x=2
fun2() # 虽然在 fun2 定义前使用,但不会报错
print(x) # >> 2
def fun2():
print(x) # >> 1
fun1()
函数在定义阶段不会执行,因此可以使用未定义的名字,但是在运行之前,这些名字必须定义好,否则会出错。
因此,上面代码不会报错。
x=1
def fun1():
x=3
g()
print(x)
fun1() # 报错
def fun2():
print(x)
这里出错,这是因为 fun1( ) 已经运行了,但是 fun2( ) 还没有定义好,所以就出错了。
x = 1
def fun1():
print(x)
def fun2():
x = 2
fun1()
输出:1
在上面 名称空间查找顺序 中说过 函数名称空间的查找顺序以定义阶段为基准,所以在定义时只有全局的
x = 1所以最后输出 1 。
