Python名称空间与作用域

MJX2022/08/21Python名称空间

名称空间

垃圾回收机制 这篇文章中我们曾讲过,在内存中变量名和变量值分别存储在 栈区堆区 中。现在我们又有了一个概念叫 名称空间(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为我们提供了globalnonlocal关键字。

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 。