楔子 在介绍栈桢的时候,我们看到了 3 个独立的名字空间:f_locals、f_globals、f_builtins。名字空间对 Python 来说是一个非常重要的概念,虚拟机的运行机制和名字空间有着非常紧密的联系。并且在 Python 中,与名字空间这个概念紧密联系在一起的还有名字、作用域这些概念,下面我们就来剖析这些概念是如何体现的。 变量只是一个名字 在这个系列的最开始我们就说过,从解释器的角度来看,变量只是一个泛型指针 PyObject *,而从 Python 的角度来看,变量只是一个名字、或者说符号,用于和对象进行绑定的。
上面这个赋值语句其实就是将 name 和 "古明地觉" 绑定起来,让我们可以通过 name 这个符号找到对应的 PyUnicodeObject。因此定义一个变量,本质上就是建立名字和对象之间的映射关系。 另外我们说 Python 虽然一切皆对象,但拿到的都是指向对象的指针,因此创建函数和类,以及模块导入,同样是在完成名字和对象的绑定。
创建一个函数也相当于定义一个变量,会先根据函数体创建一个函数对象,然后将名字 foo 和函数对象绑定起来。所以函数名和函数体之间是分离的,同理类也是如此。
导入一个模块,也是在定义一个变量。import os 相当于将名字 os 和模块对象绑定起来,通过 os 可以找到指定的模块对象。
import numpy as np 中的 as 语句同样是在定义变量,将名字 np 和对应的模块对象绑定起来,以后就可以通过 np 这个名字去获取指定的模块了。 总结:无论是普通的赋值语句,还是定义函数和类,亦或是模块导入,它们本质上都是在完成变量和对象的绑定。
里面的 name、foo、A、os、np,都只是一个变量,或者说名字、符号,然后通过名字可以获取与之绑定的对象。 作用域和名字空间 正如上面所说,赋值语句、函数定义、类定义、模块导入,本质上只是完成了变量和对象之间的绑定,或者说我们创建了变量到对象的映射,通过变量可以获取对应的对象,而它们的容身之所就是名字空间。 所以名字空间是通过 PyDictObject 对象实现的,这对于映射来说简直再适合不过了。而前面介绍字典的时候,我们说字典是被高度优化的,原因就是虚拟机本身也重度依赖字典,从这里的名字空间即可得到体现。 当然,在一个模块内部,变量还存在可见性的问题,比如:
我们看到同一个变量名,打印的确是不同的值,说明指向了不同的对象,换句话说这两个变量是在不同的名字空间中被创建的。 名字空间本质上是一个字典,如果两者在同一个名字空间,那么由于 key 的不重复性,当执行 x = 2 的时候,会把字典里面 key 为 "x" 的 value 给更新成 2。但是在外面还是打印 1,这说明两者所在的不是同一个名字空间,打印的也就自然不是同一个 x。 因此对于一个模块而言,内部可以存在多个名字空间,每一个名字空间都与一个作用域相对应。作用域可以理解为一段程序的正文区域,在这个区域里面定义的变量是有意义的,然而一旦出了这个区域,就无效了。 关于作用域这个概念,我们要记住:它仅仅是由源代码的文本所决定。在 Python 中,一个变量在某个位置是否起作用,是由它的文本位置决定的。 因此 Python 具有静态作用域(词法作用域),而名字空间则是作用域的动态体现,一个由程序文本定义的作用域在运行时会转化为一个名字空间、即一个 PyDictObject 对象。比如进入一个函数,显然会进入一个新的作用域,因此函数在执行时,会创建一个名字空间。
所以名字空间是名字、或者说变量的上下文环境,名字的含义取决于名字空间。更具体的说,一个变量绑定的对象是不确定的,需要由名字空间来决定。 位于同一个作用域的代码可以直接访问作用域中出现的名字,即所谓的直接访问;但不同的作用域,则需要通过访问修饰符 . 进行属性访问。
如果想在 B 里面访问 A 里面的内容,要通过 A.属性的方式,表示通过 A 来获取 A 里面的属性。但是访问 B 的内容就不需要了,因为都是在同一个作用域,所以直接访问即可。 访问名字这样的行为被称为名字引用,名字引用的规则决定了 Python 程序的行为。
还是上面的代码,如果我们把函数里面的 a = 2 给删掉,意味着函数的作用域里面已经没有 a 这个变量了,那么再执行程序会有什么后果呢?从 Python 层面来看,显然是会寻找外部的 a。因此我们可以得到如下结论:
global 名字空间 不光函数、类有自己的作用域,模块对应的源文件本身也有相应的作用域。比如:
这个文件本身也有自己的作用域,并且是 global 作用域,所以解释器在运行这个文件的时候,也会为其创建一个名字空间,而这个名字空间就是 global 名字空间,即全局名字空间。它里面的变量是全局的,或者说是模块级别的,在当前文件的任意位置都可以直接访问。 而 Python 也提供了 globals 函数,用于获取 global 名字空间。
里面的 ... 表示省略了一部分输出,我们看到创建的全局变量就在里面。而且 foo 也是一个变量,它指向一个函数对象。 注意:我们说函数内部是一个独立的 block,因此它会对应一个 PyCodeObject。然后在解释到 def foo 的时候,会根据 PyCodeObject 对象创建一个 PyFunctionObject 对象,然后将 foo 和这个函数对象绑定起来。 当我们调用 foo 的时候,再根据 PyFunctionObject 对象创建 PyFrameObject 对象、然后执行,至于具体细节留到介绍函数的时候再细说。总之,我们看到 foo 也是一个全局变量,全局变量都在 global 名字空间中。 总之,global 名字空间全局唯一,它是程序运行时的全局变量和与之绑定的对象的容身之所。你在任何一个位置都可以访问到 global 名字空间,正如你在任何一个位置都可以访问全局变量一样。 另外我们思考一下,global 名字空间是一个字典,全局变量和对象会以键值对的形式存在里面。那如果我手动地往 global 名字空间里面添加一个键值对,是不是也等价于定义一个全局变量呢?
我们看到确实如此,往 global 名字空间里面插入一个键值对完全等价于定义一个全局变量。并且 global 名字空间是唯一的,你在任何地方调用 globals() 得到的都是 global 名字空间,正如你在任何地方都可以访问到全局变量一样。 所以即使是在函数中给 global 名字空间添加一个键值对,也等价于定义一个全局变量。 问题来了,如果在函数里面,我们不获取 global 名字空间,怎么创建全局变量呢?
很简单,Python 为我们准备了 global 关键字,表示声明的变量是全局的。 local 名字空间 像函数和类拥有的作用域,我们称之为 local 作用域,在运行时会对应 local 名字空间,即局部名字空间。由于不同的函数具有不同的作用域,所以局部名字空间可以有很多个,但全局名字空间只有一个。 对于 local 名字空间来说,它也对应一个字典,显然这个字典就不是全局唯一的了。而如果想获取局部名字空间,Python 也提供了 locals 函数。
显然对于模块来讲,它的 local 名字空间和 global 名字空间是一样的,也就是说,模块对应的栈桢对象里面的 f_locals 和 f_globals 指向的是同一个 PyDictObject 对象。 但对于函数而言,局部名字空间和全局名字空间就不一样了。调用 locals() 是获取自身的局部名字空间,而不同函数的局部名字空间是不同的。但是 globals() 函数的调用结果是一样的,获取的都是全局名字空间,这也符合函数内不存在指定变量的时候会去找全局变量这一结论。
builtin 名字空间 Python 有一个所谓的 LGB 规则,指的是在查找一个变量时,会按照自身的 local 空间、外层的 global 空间、内置的 builtin 空间的顺序进行查找。 builtin 名字空间也是一个字典,当 local 名字空间、global 名字空间都查找不到指定变量的时候,会去 builtin 空间查找。而关于 builtin 空间的获取,Python 提供了一个模块。
builtins 是一个模块,那么 builtins.__dict__ 便是 builtin 名字空间,也叫内置名字空间。
这里提一下在 Python2 中,while 1 比 while True 要快,为什么? 因为 True 在 Python2 中不是关键字,所以它是可以作为变量名的。那么虚拟机在执行的时候就要先看 local 空间和 global 空间里有没有 True 这个变量,有的话使用我们定义的,没有的话再使用内置的 True。 而 1 是一个常量,直接加载就可以,所以 while True 多了符号查找这一过程。但是在 Python3 中两者就等价了,因为 True 在 Python3 中是一个关键字,也会直接作为一个常量来加载。 exec 和 eval 记得之前介绍 exec 和 eval 的时候,我们说这两个函数里面还可以接收第二个参数和第三个参数,它们分别表示 global 名字空间、local 名字空间。
至于 eval 也是同理:
所以名字空间本质上就是一个字典,所谓的变量不过是字典里面的一个 key。为了进一步加深印象,再举个模块的例子:
怎么样,是不是很有意思呢?相信你对名字空间已经有了足够清晰的认识,它是变量和与之绑定的对象的容身之所。 小结 名字空间是 Python 的灵魂,它规定了一个变量应该如何查找,关于变量查找,下一篇文章来详细介绍,到时你会对名字空间有更加透彻的理解。 然后是作用域,所谓名字空间其实就是作用域的动态体现。整个 py 文件是一个作用域,也是全局作用域;定义函数、定义类、定义方法,又会创建新的作用域,这些作用域层层嵌套。 那么同理,运行时的名字空间也是层层嵌套的,形成一条名字空间链。内层的变量对于外层是不可见的,但外层的变量对内层是可见的。 然后全局名字空间是一个字典,它是唯一的,操作里面的键值对等价于操作全局变量;至于局部名字空间则不唯一,每一个函数都有自己的局部名字空间,但我们要知道函数内部在访问局部变量的时候是静态访问的(相关细节后续聊)。 还有内置名字空间,可以通过 __builtins__ 获取,但拿到的是一个模块,再获取它的属性字典,那么就是内置名字空间了。 |
|