一文了解 Python 中的命名空间和作用域

本文主要介绍 Python 的命名空间和作用域,以及 nonlocal 和 global 的用法。阅读本文预计需要 15 min。

1. 前言

Python 命名空间(Namespace)作用域(Scope)对于所有 Python 程序员都非常有用。弄明白 Python 命名空间和作用域对于我们 Python 编程和调试 Bug 都很有用!

为什么需要命名空间和作用域呢?从设计层面,我目前的理解是为了计算机正确执行代码。有了规范协议,按照规范协议写代码,开发人员和计算机就能互相理解对方,默契配合,代码也就能正确执行。把这些当做一个规范协议,符合规范的代码,才能正确执行,不然就是 Bug 了。

本文主要内容:

  • 命名空间
  • 作用域
  • 全局变量 VS 局部变量
  • global VS nonlocal

2. 命名空间

命名空间(Namespace):指的是从名字(Name)对象(Object)映射(Mapping)。可以把它理解为一张“映射表”

这像我们初中学的映射关系,命名空间是名字和对象的一张映射表。通过名字,我们就可以找到这个对象的位置。如同,名字是钥匙,对象是房间,一把钥匙打开一间房间。

命名空间是互相独立的。关于命名空间的重要一点是,不同命名空间中的名称之间绝对没有关系,在不同的映射表中可以出现相同的名字,而不会出问题,例如,两个不同的模块都可以定义一个 maximize 函数而不会产生混淆,只要模块的用户在其前面加上模块名称。

Python 中常见的命名空间有:

  • 存放内置函数的集合(包含 abs() 这样的函数,和内建的异常等),可以认为是”内置函数映射表“。
  • 模块中的全局名称,可以认为是“模块映射表”。
  • 函数调用中的局部名称,可以认为是“函数映射表”。

在不同时刻创建的命名空间拥有不同的生命周期。:

  • 内置名称的命名空间:是在 Python 解释器启动时创建的,永远不会被删除,直到退出 Python。如:Python 内置的关键字、函数等就放在这个里面。
  • 模块的全局命名空间:在模块定义被读入时创建;通常,模块命名空间也会持续到解释器退出。被解释器的顶层调用执行的语句,从一个脚本文件读取或交互式地读取,被认为是 __main__ 模块调用的一部分,因此它们拥有自己的全局命名空间。如:我们定义的全局变量就放在模块的全局命名空间。(内置名称实际上也存在于一个模块中;这个模块称作 builtins 。)
  • 函数的局部命名空间:在这个函数被调用时创建,并在函数返回或抛出一个不在函数内部处理的错误时被删除。(事实上,比起描述到底发生了什么,忘掉它更好。)当然,每次递归调用都会有它自己的局部命名空间。如:我们定义的局部变量就放在局部命名空间。

相信现在,大家对于命名空间有了基本的理解,简单说,它就是一个名字和对象的映射表。一个程序运行的时候,会生成很多个映射表(命名空间),那么这些命名空间有什么用呢?接下来就介绍作用域。

3. 作用域

作用域(Scope): 是一个命名空间(映射表)可直接访问的 Python 程序的文本区域。 这里的 “可直接访问” 意味着对名称的非限定引用会尝试在不同命名空间中查找名称。

这是官方文档给的解释。我的理解是:把源代码(程序)比作中国的话,命名空间(映射表)就是各级人民政府,作用域就好比各级人民政府管辖的区域(代码文本片段),即映射表管理的代码文本片段。如:一个函数的命名空间(映射表)管理的范围就是这个函数代码文本。

我们知道中央可以管理地方,地方不可以管理中央。命名空间(映射表)也是一样的,有些命名空间(映射表)可以管理整个代码,有些命名空间(映射表)只能管理一个函数代码。那么问题来了,这么多命名空间(映射表),Python 是怎么找到需要的对象的呢?

其实,跟政府管理时,一级一级向上报告一样,Python 也是一级一级的不同的命名空间(映射表)查找,如果所有的映射表都找不到,就报错。

查找的顺序就是之前说过的 LEGB 原则。

作用域是静态确定,动态使用。 在 Python 程序运行的任何时间,至少有 3 个命名空间是能够直接访问的作用域:

  1. 首先搜索包含局部名称(局部变量)的最内部的作用域。这是最先查找的命名空间(映射表),可以当作村委,处在基层,最下面的位置。(Local, scope L)

  2. 从最近的闭包作用域开始搜索的任何闭包函数的作用域,如:非局部名称(non-local 变量)、非全局名称(non-global)等。简单说就是存在函数嵌套时,一级一级往上层函数的命名空间查找。Enclosing scope, E)

  3. 倒数第二个作用域是包含当前模块全局名称(全局变量)的命名空间(映射表)。如:全局变量。(Global scope, G)

  4. 最后搜索最外面的作用域,即包含内置名称(内置函数、关键字等)的命名空间(映射表)。(Builtin scope, B)

注意:类定义单独放在一个局部作用域的命名空间。

重要的是应该意识到作用域是按代码文本来确定的:在一个模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。 另一方面,实际的名称(变量)搜索是在运行时动态完成的 — 但是,Python 正在朝着“编译时静态名称解析”的方向发展,因此不要过于依赖动态名称解析! (事实上,局部变量已经是被静态确定了。)这部分了解一下就行。

4. 全局变量 VS 局部变量

全局变量通常是指在函数外定义的变量,它的引用和赋值都是跟模块的命名空间(映射表)关联,所以它的声明生命周期和模块一样,直到程序结束执行,或者这个全局变量被删除,它就从映射表中被删除。

局部变量通常是指函数内定义的变量。这个函数被调用时,创建这个函数的命名空间(映射表),函数调用结束时删除。

举个栗子:

city = '武汉'  # 这是全局变量,作用域是全局,放在模块命名空间(映射表)

def my_info():
    name = 'Jock'  # 这是局部变量,作用在局部
    print(f"打印局部变量name:{name}")
    print(f"打印全局变量city:{city}")  # 这时city变量是在模块命名空间(映射表)找到

my_info()
print(f"在函数外打印全局变量city:{city}")

# NameError: name 'name' is not defined,因为name变量作用在函数my_info,
# 函数调用结束,函数的命名空间被删除,name 不存在了
# print(f"在函数外打印局部变量name:{name}")

输出结果:
打印局部变量name:Jock
打印全局变量city:武汉
在函数外打印全局变量city:武汉

这里说明一下,name 是局部变量,放在 my_info 函数的命名空间,只作用于函数里面,所以无法在函数外使用。

如果在函数内尝试修改全局变量的话,那么 Python 会在函数内重新创建一个新的局部变量,这个变量的名字和全局变量名字一样,只不过局部变量放在函数命名空间,全局变量放在模块命名空间。

测试如下:

city = '武汉'  # 这是全局变量,作用域是全局,放在模块命名空间(映射表)

def my_info():
    city = '桂林'
    print(f"函数内变量city的内存地址是:{id(city)}")
    print(f"在函数内打印变量city:{city}")

my_info()
print(f"函数外变量city的内存地址是:{id(city)}")
print(f"在函数外打印变量city:{city}")

输出结果:
函数内变量city的内存地址是:2619120672560
在函数内打印变量city:桂林
函数外变量city的内存地址是:2619120920496
在函数外打印变量city:武汉

可以发现函数里的变量 city 和函数外的变量 city 内存地址不一样,说明是不同的两个对象。

那么如何实现在函数内修改全局变量呢?接下来就介绍两个关键字 globalnonlocal

5. global VS nonlocal

global:用于声明全局变量,可以在函数内声明全局变量,然后实现对全局变量的修改。

举栗子:

city = '武汉'  # 这是全局变量,作用域是全局,放在模块命名空间(映射表)

def my_info():
    global city  # 声明city为全局变量
    city = '桂林'  # 这时修改的是全局变量
    print(f"函数内变量city的内存地址是:{id(city)}")
    print(f"在函数内打印变量city:{city}")

my_info()
print(f"函数外变量city的内存地址是:{id(city)}")
print(f"在函数外打印变量city:{city}")

输出结果:
函数内变量city的内存地址是:2042908827440
在函数内打印变量city:桂林
函数外变量city的内存地址是:2042908827440
在函数外打印变量city:桂林

可以发现,成功实现了对全局变量的修改。
使用 global 有一些注意事项:

  1. global 后声明的变量在本作用域中必须没有被使用。global citycity = '桂林' 互换位置会报错。这有点必须先声明后使用的感觉。
  2. global 后声明的变量不能用于函数的正式形参、 for 循环的控制目标、class 定义、函数定义、import 语句和变量标注。这部分的话,没啥经验,我也不太清楚,读者有知道的,欢迎留言交流。

nonlocal:在嵌套函数中,在内层函数声明 nonlocal 可以实现修改外层函数的局部变量值。

举栗子:

def my_info():
    city = "武汉"  # 外层函数局部变量
    def my_city():
        city = "桂林"
        print(f"在内层函数变量city的内存地址是:{id(city)}")
        print(f"在内层函数打印变量city:{city}")
    my_city()
    print(f"在外层函数变量city的内存地址是:{id(city)}")
    print(f"在外层函数打印变量city:{city}")

my_info()

结果输出:
在内层函数变量city的内存地址是:1265469990832
在内层函数打印变量city:桂林
在外层函数变量city的内存地址是:1265469742896
在外层函数打印变量city:武汉

可以发现,在嵌套函数中,外层函数中的变量,相对于内层函数来说,只可读不可修改。在内层函数尝试修改外层函数的变量,会在内层函数重新创建一个新的局部变量,而外层函数的变量依然存在。注意,在内部函数中不能先访问外部函数变量,之后再定义一个同名的局部变量,这样也会报错。如下:

def my_info():
    city = "武汉"  # 外层函数局部变量
    def my_city():
        print(f"在内层函数访问外层函数变量city:{city}")
        city = "桂林"
        print(f"在内层函数变量city的内存地址是:{id(city)}")
        print(f"在内层函数打印变量city:{city}")
    my_city()
    print(f"在外层函数变量city的内存地址是:{id(city)}")
    print(f"在外层函数打印变量city:{city}")

my_info()

输出结果:
UnboundLocalError: local variable 'city' referenced before assignment

即 Python 解释器会认为你的局部变量没有赋值定义就使用了,所以报错。

那么如何实现内部函数修改外部函数局部变量呢?通过 nonlocal 实现。

def my_info():
    city = "武汉"  # 外层函数局部变量
    def my_city():
        nonlocal  city
        city = "桂林"
        print(f"在内层函数变量city的内存地址是:{id(city)}")
        print(f"在内层函数打印变量city:{city}")
    my_city()
    print(f"在外层函数变量city的内存地址是:{id(city)}")
    print(f"在外层函数打印变量city:{city}")

my_info()

输出结果:
在内层函数变量city的内存地址是:2628557791024
在内层函数打印变量city:桂林
在外层函数变量city的内存地址是:2628557791024
在外层函数打印变量city:桂林

可以发现,通过 nonlocal 成功实现了在内层函数修改外层函数局部变量。
使用 nonlocal 需要注意:nonlocal 后面跟着的变量必须是外层函数的局部变量,否则会报错。SyntaxError: no binding for nonlocal 'XXX' found。如:

def my_info():
    city = "武汉"  # 外层函数局部变量
    def my_city():
        nonlocal  city
        nonlocal  name
        city = "桂林"
        print(f"在内层函数变量city的内存地址是:{id(city)}")
        print(f"在内层函数打印变量city:{city}")
    my_city()
    print(f"在外层函数变量city的内存地址是:{id(city)}")
    print(f"在外层函数打印变量city:{city}")

my_info()

输出结果:
SyntaxError: no binding for nonlocal 'name' found

下面展示一个官方文档给的例子,如果能够看明白,下面的例子,相信大家已经彻底搞明白了 globalnonlocal 的用法,对于作用域和命名空间也有了更深的认识。
global 和 nonlocal 的用法:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

输出结果:
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

说明:
可以看到内部函数 do_local() 不能修改外部函数 scope_test() 的局部变量,如果内部函数要修改外部函数的局部变量,需要用 nonlocal 关键字。通过 do_nonlocal() 的方式可以实现修改外部函数 scope_test() 的局部变量 spam,所以 spam 经过 do_nonlocal() 变为了 nonlocal spam 。内部函数 do_global() 中用 global 关键字声明了一个全局变量,然后对全局变量 spam 进行赋值(如果已存在全局变量 spam,则会修改原全局变量值),但是这不会影响 scope_test() 函数中局部变量 spam 的值,因为这两个 spam 存放在不同的命名空间,外层函数中的 spam 依然是 nonlocal spam

Python 的一个特殊规定是这样的:如果不存在生效的 global 或 nonlocal 语句 – 则对名称的赋值总是会进入最内层作用域。 赋值不会复制数据 — 它们只是将名称(变量)绑定到对象。 删除也是如此:语句 del x 会从局部作用域所引用的命名空间中移除对 x 的绑定。 有点像给一个对象贴标签(赋值)和撕标签(删除)的过程。

事实上,所有引入新名称的操作都是使用局部作用域:特别地,import 语句和函数定义会在局部作用域中绑定模块或函数名称。

6. 小结

  1. 命名空间是一个名字(变量)和对象的映射表。
  2. 作用域是指命名空间的作用范围,或者说管辖区域。
  3. 变量的查找遵循 LEGB 原则,先从基层(最内层函数找),然后到市委(外层函数)…,再到省委(模块命名空间),最后到中央(builtin 命名空间)。
  4. 各个命名空间相互独立,创建时间和生命周期各不相同。
  5. global 用于在函数内创建和修改全局变量。
  6. nonlocal 用于在内层函数修改外层函数局部变量。
  7. 没有声明 global 和 nonlocal,尝试修改全局变量或外层函数局部变量,实际上只会在函数或者内层函数创建一个新的局部变量,同名的全局变量或者外层函数局部变量不会受影响。

7. 巨人的肩膀

  1. Pytho 官网作用域和命名空间英文版
  2. The global statement and The nonlocal statement

推荐阅读:

  1. 编程小白安装Python开发环境及PyCharm的基本用法
  2. 一文了解Python基础知识
  3. 一文了解Python数据结构
  4. 一文了解Python流程控制
  5. 一文了解Python函数
  6. 一文了解Python部分高级特性
  7. 一文了解Python的模块和包

后记:
我从本硕药学零基础转行计算机,自学路上,走过很多弯路,也庆幸自己喜欢记笔记,把知识点进行总结,帮助自己成功实现转行。
2020下半年进入职场,深感自己的不足,所以2021年给自己定了个计划,每日学一技,日积月累,厚积薄发。
如果你想和我一起交流学习,欢迎大家关注我的微信公众号每日学一技,扫描下方二维码或者搜索每日学一技关注。
这个公众号主要是分享和记录自己每日的技术学习,不定期整理子类分享,主要涉及 C – > Python – > Java,计算机基础知识,机器学习,职场技能等,简单说就是一句话,成长的见证!
每日学一技

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值