命名空间是Python程序运行的一个重要概念,变量只有在具体的命名空间中才有实际的意义。犹如图书馆里的书架编号,01001书架只有在具体的图书管理区域内才能标识实际的位置。老规矩,言谈不是我的长处,代码演示更能表达我的观点。
输出结果: 3 和 5, 未报错
输出结果:3 和 5, 未报错
输出结果:3 和 1, 未报错
输出结果:报错, 未绑定变量 'b'
这是怎么回事呢,接下来我们从Python源码的角度结合运行时信息来探索一下Python变量作用域规则的奥秘。
一般来说,在Python内部,每个独立的代码段都有属于自己的独立的命名空间,模块、类、函数都有独立的命名空间。在第一和第二个示例中,变量b在模块的命名空间中(每个py文件可视为一个模块),而a则在func 代码段的空间中,那么变量b又是怎么被func函数识别呢?
Python源代码被执行,首先要先编译成字节码,然后生成Python运行时栈帧环境,那么接下来就看一下他们字节码和栈帧环境是什么样的。
第一和第二个程序对应栈帧环境几乎是一样的,在func内部打印变量b时,使用的是LOAD_GLOBAL,说明变量b对于func作用域来说是一个全局变量,全局变量与在在源码中的定义位置无关。
第三段程序在打印变量b的时候,使用的是LOAD_FAST字节码,说明b对于func作用来说是个内部的局部变量。
第四段程序也是把变量b作为局部变量来处理的,只是变量b和对应值1之间的约束关系在print语句之后才绑定的,所以就出现了UnboundLocalError异常。
为什么作为全局变量b,可以在模块的任意位置定义,而局部变量b在func内部和定义位置顺序相关呢?
Python的运行时栈帧环境通过PyFrameObject对象来模拟的,定义如下:
对于func来说,全局变量都保存在f_global中,而它是一个PyDictObject对象,也就是字典对象。恍然大悟。。。原来字典本来就是无序的。Python程序运行的过程是先经过编译,然后才执行,所以全局变量定义与位置顺序无关。
为什么第四段程序会报错而第三段没有问题呢,从表面上来看,似乎和定义的先后顺序有关,那到底是怎么相关的呢。通过查看Python的栈帧运行时信息,原来秘密藏在f_localsplus中,被定义为一个长度为1的指针数组(如果长度大于1会自动分配内存)。
我们的都知道,Python是完全面向对象的,也就是说你所能看到的所有的东西都是对象,函数也不例外。func在内部也是一个对象是一个PyFunctionObject,在生成运行时栈帧的过程中,需要初始化参数和局部变量,而这些信息对于函数对象来说都是已知的确定,所以Python在实现对局部变量存取的时候采用了列表的形式,这样可以提高Python运行效率。f_localsplus保存局部变量的规则:参数和位置参数按顺序放在前面,扩展参数紧随其后。f_localsplus包含四部分内容,通过偏移量进行取值。
extras = f->f_code->co_stacksize + f->f_code->co_nlocals +
ncells + nfrees;
通过第四段程序的栈帧字节码可以看出,变量b取值在前,而约束绑定(赋值)在后,就报出了UnBoundLocalError的错误。
领取专属 10元无门槛券
私享最新 技术干货