山地人

Python 命名空间和作用域

山地人
山地人
2021-07-02

这一篇我们会学习:

  1. 什么是变量名?
  2. 什么是命名空间?
  3. 变量名称是如何和对象(Object)建立映射关系?
  4. 什么是变量的作用域?

什么是名字

在Python的世界里,所有的东西都是对象,但这些对象其实就是保存在内存上的一些数据。为了让我们能否访问到这些内存上的数据,编程语言大都会提供一种通过变量名称的方式来访对应数据的方法。

这个变量名,就好比是各种现实世界的门牌号,你通过门牌号可以找到要去的具体地址。

Python提供了一个叫id()的函数,使用这个函数,我们可以获取出对象的内存地址。

下面,我们再来看一组实验,如果两个变量的值都是100,他们的地址是否真的相同?

另外,你还可以把x和y对应的100换成两个一样的数组,比如: [1,2,3],看看结果发生了什么变化?

Python这样的设计,可以避免创建大量的重复对象。

命名空间

看到上面这段代码,是否会疑惑最后一行x输出的究竟是10还是20。如果你和我一样,都有这样的困惑,那么要搞清楚这个问题,就得弄明白命名空间的概念了。

命名空间:你可以把它看成是一个用来管理一群名称的集合。Python程序运行时,通过查找这些命名空间,来找到你要的名称所对应的对象。

在Python的世界里,有四类命名空间:

  1. 内置命名空间 Built-in
  2. 全局命名空间 Global
  3. 封闭命名空间 Enclosing
  4. 局部命名空间 Local

这些命名空间都拥有各自的生命周期,何时创建,何时销毁都有各自的一套规则。

下面我们分别来看下:

内置命名空间 Built-In

在Python程序运行时,会自动创建内置命名空间,这个命名空间里放置了Python内置的函数、变量。

Python提供了一个访问内置命名空间的字段:__builtins__

启动下面的终端,看看内置命名空间下都有哪些成员?

平时经常使用的print()()int()行数都是来自于内置命名空间

内置命名空间的生命周期:随着程序的创建而创建,随着程序的程序的消亡而消亡。伴随程序走完一生

全局命名空间 Global

内置命名空间一样,全局命名空间的生命周期也是伴随程序一生的。

一个Python程序可以有多个全局命名空间。你在主程序中定义的函数和变量会有各自的全局命名空间,通过import导入的每个module都会创建各自的全局命名空间

Python提供了一个查看所有全局命名空间的函数:globals()

动动手

  1. 启动终端,看看全局命名空间下都有些什么。
  2. 定义一个新变量,比如:x = 123
  3. 再次运行globals()函数,看看是否能找到这个新定义的x
  4. 定义一个函数试试。

本地命名空间

foo()函数在被调用运行的时候,会创建一个新的命名空间,在这个命名空间内部会放置函数内部定义的x变量。

在函数运行结束后,这个x不在有任何对象使用,所以也会随之消亡。这个命名空间被称为本地命名空间或者叫做局部命名空间。和全局命名空间不同之处是,全局命名空间里的名字,在函数内部是可以访问到的,但是本地命名空间只是这个命名空间内部的成员可以访问到。外部的代码是无法访问的。

封闭命名空间

有些时候,可能会在函数内部再定义一些函数,比如上面的foo()函数内部又定义了一个bar()

下面我们分析下这个函数的运行过程:

  1. foo()被调用的时候,创建了一个命名空间,这个命名空间是foo的本地命名空间,里面有一个x和一个函数bar 2.在运行bar()函数的时候,Python解释器又创建了一个bar本地命名空间,这时,对于bar来说,之前那个foo的命名空间就被称为bar封闭命名空间。foo可以访问外部的这个bar的封闭命名空间

但是,bar内部是不能直接修改foo中的x的值的。

上面的bar中有一行x = 456,这行代码在运行的时候,实际上是在bar的命名空间中创建了一个新的x而不是修改foo中的x

修改封闭命名空间里的数据

如果你非要修改foo中的x,那代码要做下小调整。

bar()x = 456前面加上一句nonlocal x。这样当执行bar()函数时,解释器会知道你要绑定的x不是本地命名空间里的x而是会往外寻找离bar最近的封闭空间里的x,一旦找到了这样的x(也就是这个例子中foo的命名空间里的x),立马在当前foo的本地命名空间里绑定这个x。之后x = 456的赋值操作,会修改掉foo中x的值。

你可以运行一下上面沙盒的例子,体会下这个过程。

修改全局命名空间里的数据

同样的,如果你要在一个函数内部修改全局命名空间里的数据,Python也提供了一个关键字global来帮助你在局部命名环境中绑定全局命名环境中的名字。

还是把之前的例子做一下修改,然后在bar中使用global关键字声明,绑定全局环境下的x。运行下上面的例子,看看最后在全局命名环境下最后打印的x会是什么值?

命名查找机制

在访问一个变量数据的时候,Python在查找名字时会遵循一套机制:

  1. 对于不是以global或者nolocal声明的变量名,解释器会先查找局部命名空间下是否存在这个名字,如果找到,就立刻读取这个变量的值。
  2. 如果没有在局部命名空间中找到这个变量时,解释器会往外一层,在外部的封闭命名空间中查找,一层一层往外,直到找到一个匹配名称的变量,立即读取这个变量的值。
  3. 如果在封闭命名空间中也没有找到,那解释器会继续查找全局命名空间中是否有这个名字。
  4. 如果依然没有找到,最后解释器会来到内置命名空间中查找是否有这个名字。
  5. 如果依然没有,解释器会抛出异常报告。

动动手

你可以依次删除第5行第3行第1行x = ...的语句,观察bar()内部的print(x)每次打印的值是如何变化的。

作用域

编程里的作用域,你可以想象成一个学校的领导班子。

本地命名空间的作用域

一个函数在运行的时候创建里一个本地命名空间,这个函数就好比是一个班级,每个函数在运行的时候,函数内部的事情自己管理,就好比班级自己管理好班级内部的事务一样。如果在函数内部要查找一个名字,就现在函数内部的这个局部命名空间里查找。就好比要找一个叫张三的同学,先在班级内部查找。所以对于本地命名空间它的作用域就只限制在这个函数内部,就好比一个班级的管理权限就只限于这个班级。

封闭命名空间的作用域

对于那种带有嵌套的函数,最内层的函数可以读取外层的封闭命名空间里的信息,但如果不进行特殊声明,一般不能修改封闭空间里的数据,就好比学校里的每个年级组和这个年级组里的每个班级的关系,年级组的信息对于这个年级的每个班级,就好比是封闭命名空间和本地命名空间的关系。年级组有信息可以和每个年级组内部的班级共享信息。

全局命名空间的作用域

全局命名空间的作用范围最广,就好比整个学校和学校里每个班级的关系。继续之前找人的故事,如果在班级里找一个叫张三的同学,没有找到,就去这个班级所在年级找,如果找不到就扩大范围在全校范围找。

内置命名空间的作用域

内置命名空间,就好比是学校里设置的办公室。每个教室里要老师上课,校方会安排各个办公室的老师去给学生上课。我们在函数或者全局环境里运行print函数,这个print函数就是来自办公室的。

读到这里,相信你对各种命名空间和各自的作用域应该有了更为深刻的认识。这次的内容就先写到这里。

至此,本篇教程也到了该和你说再见的时候了,我们下期再见。

学完本篇互动教程,如果你觉得体验不错,可以把网页链接发送给你的小伙伴,让他/她也来感受一下。当然,你也可以继续看看网站上其他的的互动教程,希望`idev365`能够给你带来收获。

学习教程的过程中碰到了问题,或者对idev365有什么改进意见和想法,欢迎加入idev365微信内测群,和山地人交流你的想法。