Python 作用域(scope)和命名空间(namespace)
先看官方文档(节选)
https://docs.python.org/zh-cn/3/tutorial/classes.html#python-scopes-and-namespaces
命名空间(namespace)是映射(mapping)到对象的名称。
关于命名空间的一个重要知识点是,不同命名空间中的名称之间绝对没有关系;例如,两个不同的模块都可以定义 maximize 函数,且不会造成混淆。用户使用函数时必须要在函数名前面附加上模块名。
变量作用域指的是变量生效的范围。变量的作用域决定了在哪一部分程序你可以访问哪个特定的变量名称。
作用域(scope) 是命名空间可直接访问的 Python 程序的文本区域(textual region)。 “可直接访问” 的意思是,对名称的非限定引用会在命名空间(namespace)中查找名称。
作用域虽然是静态确定的,但会被动态使用。执行期间的任何时刻,都会有 3 或 4 个命名空间可被直接访问的嵌套的(nested)作用域,优先顺序:
首先搜索最内层作用域包含的局部(local)名称;
从最近的外层/封闭(enclosing)作用域开始搜索的任何外层/封闭函数的作用域都包含非局部且非全局的名称;
【注:中文在线帮助将enclosing functions译为外层函数,有些资料称为封闭函数。
函数在执行时使用函数局部变量符号表,所有函数变量赋值都存在局部符号表中;引用变量时,首先,在局部(local)符号表里查找变量,然后,是外层函数(enclosing functions)局部符号表,再是全局(global)符号表,最后是内置(built-in)名称符号表。因此,尽管可以引用全局变量和外层函数的变量,但最好不要在函数内直接赋值(除非是 global 语句定义的全局变量,或 nonlocal 语句定义的外层函数变量)。见
https://docs.python.org/zh-cn/3/tutorial/controlflow.html#defining-functions 】
倒数第二层作用域是当前模块的全局(global)名称;
最后搜索,最外层的作用域是包含内置( built-in )名称的命名空间。
上面是节选自官方文档,不太好理解,下面解读之。
命名空间(名称空间Namespace),是名称到对象的映射。它表示着一个标识符(identifier)的可见范围。在编程语言中,名字空间是对作用域的一种特殊的抽象,在一些编程语言(例如C++和Python)中,名字空间本身的标识符也属于一个外层的名字空间,也即名字空间可以嵌套。
命名空间提供了在项目中避免名字冲突的一种方法。命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响—— 不同命名空间中的名称之间绝对没有关系,请牢记这一点。
多个名称(在多个作用域内)可以绑定到同一个对象。这在其他语言中称为别名。在处理不可变的基本类型(数字,字符串,元组)时可以安全地忽略它。但是,对于可变对象,如列表,字典和大多数其他类型在某些方面表现得像指针,如果函数修改了作为参数传递的对象,调用者将看到更改。
在python中,任何事物都是一个对象,用名称(Name)标识。名字是访问底层对象的途径(way)。
几个命名空间的例子:存放内置函数的集合(包含 abs() 这样的函数名,和内置的异常名称 BaseException、Exception 等等);模块中的全局名称;函数中的局部名称,包括函数的参数和函数中的变量。 从某种意义上说,对象的属性集合也是一种命名空间的形式。
在不同时刻创建的命名空间拥有不同的生存期。包含内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。模块的全局命名空间在模块定义被读入时创建;通常,模块命名空间也会持续到解释器退出。被解释器的顶层调用执行的语句,从一个脚本文件读取或交互式地读取,被认为是 __main__ 模块调用的一部分,因此它们拥有自己的全局命名空间。(内置名称实际上也存在于一个模块中;这个模块称作 builtins 。)一个函数的局部命名空间在这个函数被调用时创建,并在函数返回或抛出一个不在函数内部处理的错误时被删除。当然,每次递归调用都会有它自己的局部命名空间。
变量的作用域(Scopes)决定了在哪一部分程序可以访问哪个特定的变量名称。
程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。
前面算是概论,下面进行解释。
名称 (名字Name)
前面提到,在python中,任何事物都是一个对象,用名称 (Name)标识。名称是访问底层对象的途径(way)。
例如:当我们做赋值a = 2,这里的2是一个存储对象在内存,a是一个名称,我们可以联系到它。我们可以通过内置函数id()获取到某些对象的地址(在内存),参见下图:
在这里,两者(2和a)都指向同一个对象。
下面这段代码
x = 2 # x对象被创建,指向对象2
x = x + 1 # 一个新的x对象(不同于前一行的x对象)被创建,指向对象3
y = 2 # 指向对象2
参见下图:
解释,参见下图
python不能创建新的重复对象,名称(名字)是动态绑定的,这个特征使python很强大。下图展示a引用三种不同类型的对象实例
所有这些都是有效的。
函数也是对象,因此名称也可以引用它。
def printHello():
print("hello")
a = printHello()
命名空间(名称空间Namespace)
命名空间是一个名字的集合。
在Python中,可以将名称空间想象为已定义的每个名称到相应对象的映射。
不同的名称空间可以在给定的时间共存,但它们是完全隔离的。
当我们启动Python解释器时,会创建一个包含所有内置名称的名称空间,只要不退出就存在。
这就是为什么我们可以从程序的任何部分使用诸如id()、print()等内置函数。每个模块都创建自己的全局命名空间。
这些不同的名称空间是孤立的。因此,不同模块中可能存在的相同名称不会发生冲突。
Python程序中的每个名称(变量名、函数名、类名)都有一个作用域(scope),即它所在的命名空间(namespace)。在它的作用域之外,该名称不存在,任何对它的引用都会导致错误。
Python解释器(interpreter)如何将一个名称确定为局部(local)名称还是全局(global)名称?
每当Python解释器需要计算一个名称(变量、函数等)时,它都会按以下顺序搜索名称:
1.首先局部(L,local)名称空间:自定义函数内部的变量名。
2.外层/封闭(E,enclosing)函数名称空间:如嵌套函数的外层函数中的变量名。
3.然后是全局(G,global)名称空间:如模块中函数外。
4.最后是内置(B,builtins)模块的名称空间:如内置模块预定义的变量名称。
优先顺序: L –> E –> G –>B。若最后找不到,抛出异常。下面给出示意图:
例子:
n=1
def f():
n=2 #再删除或注释掉该句看看
def g():
n=3 #先删除或注释掉该句看看
print(n)
g()
f()
运行测试如下:
使用类(class)的情况
当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域,所有对局部变量的赋值都是在这个新命名空间之内。
如果同样的属性(attribute)名称同时出现在实例和类中,则属性查找会优先选择实例。示例如下:
class Person:
name = "小芳" # 定义类变量
def __init__(self):
self.name = "小蕾" # 定义实例变量。删掉或注释掉该句再运行看看
self.age = 10
mary = Person() #类实例化
print(mary.name)
运行测试如下:
命名空间的生命周期
不同的命名空间在不同的时刻创建,有不同的生存期。
1、内置命名空间在 Python 解释器启动时创建,会一直保留,不被删除。
2、模块的全局命名空间在模块定义被读入时创建,通常模块命名空间也会一直保存到解释器退出。
3、当函数被调用时创建一个局部命名空间,当函数返回结果 或 抛出异常时,被删除。每一个递归调用的函数都拥有自己的命名空间。
变量作用域(变量作用范围 Variable Scope)
虽然定义了不同的惟一名称空间,但我们可能无法从程序的每个部分访问所有这些名称空间。范围的概念开始发挥作用。
Scope是程序的一部分,在这里可以不使用任何前缀直接访问命名空间。
在任何给定时刻,至少有三个嵌套作用域。
具有局部(local)名称的当前函数的作用域
具有全局(global)名称的模块的作用域
具有内置(built-in)名称的最外层作用域
如果在另一个函数中有一个函数,则有一个新的作用域——嵌套函数的外层函数的作用域——Enclosing(外层/封闭)作用域。
例如:
g_count = 0 # 全局(global)作用域
def outer():
o_count = 1 # Enclosing作用域
def inner():
i_count = 2 # 局部(local)作用域
搜索使用顺序: L –> E –> G –> B。
在局部找不到,便会去局部外的局部找(即嵌套函数的外层函数找),再找不到就会去全局找,再者去内置中找。如何理解?见下面具体的例子:
先给出第一段测试代码
int = 0
def fun1():
int = 1
def fun2():
int =2
print(int)
fun2()
fun1()
运行之,参见下图:
函数 fun1() 的作用就是调用函数 fun2() 来打印 int 的值,因为 local 中的 int = 2,输出2
将上述代码函数 fun2() 中的 int = 2 删除或用#注释掉,运行之,参见下图:
因为 local 找不到 int 的值,就去上一层 Enclosing中 寻找,发现 int = 1 输出1。
进一步删除函数 fun1() 中的 int = 1或用#注释掉,运行之,参见下图:
因为 local 和Enclosing 都找不到 int 的值,便去 global 中寻找,发现 int = 0 输出0。
若再删除 int = 0或用#注释掉,运行之,参见下图:
因为 local、Enclosing、global 中都没有 int 的值,便去 built-in 中寻找 int 的值,输出
Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问,示例代码如下:
if 2>1:
msg = "对啦"
else:
msg = "错啦"
print(msg)
运行之,结果参见下图:
例中 msg 变量定义在 if 语句块中,但外部还是可以访问的。
如果将 msg 定义在函数中,则它就是局部变量,外部不能访问。示例代码如下:
def test():
if 2>1:
msg = "对啦"
else:
msg = "错啦"
print(msg) #此句将报错
运行之,结果参见下图:
从报错的信息上看,说明了 msg未定义,无法使用,因为它是局部变量,只有在函数内可以使用。
关于命名空间和作用域的例子,又如:
def outer_function():
b = 20
def inner_func():
c = 30
a = 10
变量a是在全局命名空间里。变量b是在outer_function(),c是内嵌到inner_function()局部命名空间。
相对于inner_function(),c是局部的,b是非局部的——嵌套函数的外层函数的变量,a是全局的。我们既可以读取也可以为c赋值,但只能从inner_function()读取b和a。
如果我们试图将值赋给b,那么在局部命名空间中会创建一个与非局部b不同的新变量b。当我们将值赋给a时,会发生同样的情况。
但是,如果我们声明a为全局的,那么所有的引用和赋值都指向全局a。同样,如果我们想重新绑定变量b,它必须声明为非局部的。下面的例子将进一步阐明这一点:
def outer_function():
a = 20
def inner_function():
a = 30
print('a =',a)
inner_function()
print('a =',a)
a = 10
outer_function()
print('a =',a)
运行之,输出结果参见下图:
global 语句
global 语句是作用于整个当前代码块的声明。 它意味着所列出的标识符将被解读为全局变量。【https://docs.python.org/zh-cn/3/reference/simple_stmts.html#global 】简单地说,由global标出的变量是全局变量,使用global关键字,可以在函数中修改全局变量的值。
在下面这个程序中,演示了三个不同的变量a在不同的名称空间中定义,并相应地进行访问。
下面程序使用了关键字global:
def outer_function():
global a
a = 20
def inner_function():
global a
a = 30
print('a =',a)
inner_function()
print('a =',a)
a = 10
outer_function()
print('a =',a)
运行之,输出结果参见下图:
在这里,由于使用了关键字global,所有引用和赋值都指向全局a。
关键字global再举例一例:修改全局变量 n,示例代码如下:
n = 1
def fun1():
global n # 需要使用 global 关键字声明
print(n)
n = 'abc'
print(n)
fun1()
print(n)
运行之,输出结果参见下图:
nonlocal 语句
nonlocal语句使列出的标识符引用之前在最近的外层/封闭作用域(enclosing scope)中先前绑定的变量(bound variables),不包括全局变量。【https://docs.python.org/zh-cn/3/reference/simple_stmts.html#nonlocal 】简单地说,由nonlocal标出的标识符将被解读为嵌套函数的外层函数中的变量——想修改外层函数中的变量,必须加nonlocal声明。
如果要修改嵌套作用域(enclosing 作用域,外层非全局作用域)中的变量则需要 nonlocal 关键字了, 示例代码如下:
def outer():
m = 10
def inner():
nonlocal m # nonlocal关键字声明
m = 'abc'
print(m)
inner()
print(m)
outer()
运行之,输出结果参见下图:
参考:Python Namespace and Scope of a Variable (With Examples)
补充:
namespace(命名空间)术语 https://docs.python.org/zh-cn/3/glossary.html#term-namespace
nested scope(嵌套作用域)术语 https://docs.python.org/zh-cn/3/glossary.html#term-nested-scope
作用域与名字空间 https://fasionchan.com/python-source/virtual-machine/scope-namespace/
Python中的命名空间、生命周期与作用域 https://zhuanlan.zhihu.com/p/400388568