首页>Python>正文

Python容易忽视的命名空间?

时间:2018-12-24 12:10:10   来源:上海尚学堂   阅读:
前言:最近在写一个程序的时候需要用到一个全局共享的变量。最初使用的时候,我的思路如下,把全局变量定义在当前模块当中。

  1. # @Author   : stefanlei

  2. # @File     : app.py

  3.  

  4.  

  5. globa_data = 0

  6.  

  7. def f1():

  8.    globa_data = 1

  9.    pass

  10.  

  11. def f2():

  12.    print(globa_data)

  13.    pass

但是在企业项目中,我肯定不能把 globa_data 变量放在这里。一切初始化的工作,我应该提前在某个单独的模块中处理,而不是和具体的业务逻辑代码搅拌到一起,像面包屑和玉米粥一样。而且实际生产中 f1和 f2也不会仅仅是这么一点代码,肯定要分模块写。到目前为止上面的代码都可以预期的执行,第二版中我把初始化的工作都放在了一个模块中。

接下来我尝试新建了一个模块,名称为 initialize(.py) 然后在这个模块中创建了一个 str类型的 globa_data 对象,接下来我又分别新建了两个模块 app1(.py) 和 app2(.py) 这里两个模块用来存放 f1 和 f2 函数。

代码如下:


  1. # @Author   : stefanlei

  2. # @File     : initialize.py

  3.  

  4. globa_data = 0

  5. print("默认值为",globa_data)


  1. # @Author   : stefanlei

  2. # @File     : app1.py

  3. from initialize import globa_data

  4.  

  5. def f1():

  6.    # 修改了 globa_data 的值为 1

  7.    globa_data = 1

  8.    print('当前值为',globa_data)


  1. # @Author   : stefanlei

  2. # @File     : app2.py

  3. from initialize import globa_data

  4.  

  5. def f2():

  6.    print('当前值为',globa_data)


然后我又新建了一个模块 mian(.py) 用来调用 f1 和 f2 ,但是输出结果令人意外。
 


  1. # @Author   : stefanlei

  2. # @File     : main.py

  3.  

  4. from app1 import f1

  5. from app2 import f2

  6.  

  7. f1()

  8. f2()

  9.  

  10. # 输出结果

  11. # 默认值为 0

  12. # f1 当前值为 1

  13. # f2 当前值为 0

最开始的默认值为 0,经过 f1 修改后变为 1 ,后面再次调用 f2 不应该是输出 1 吗?为什么还是初始值呢?这其实就是由我们要介绍的主题 命名空间 引发的问题。
 

一、什么是命名空间(namespace)?

命名空间是用来储存变量名和对象绑定关系的一个区域,在 Python 中用字典来储存。对一个对象的读写操作,实际上就是在改变这个对象所处的命名空间的值,当然这是对命名空间而言,对象的读写底层还是内存操作。

举个例子,我们创建一个字符串对象 FluentPython,并把变量 message 分配给字符串对象(为什么这么说,因为 Python 中变量不是盒子,对象在赋值前就已经创建了,变量只是便利贴)。这其实就是在当前的命名空间的字典中,增加了一对 Key 为 message 而 Value 为 FluentPython 的键值对,当我们修改它的时候,就是修改了当前命名空间字典中相应的值,后面再次访问这个变量的时候,就是新的值,下面是代码实现,我们可以通过 globals() 函数查看当前模块这个命名空间中有哪些变量与对象。


第一个输出是还未创建 message 对象时候的结果,我们看到这里面包含了大量的 Python 内省的对象,包括 __package__、 __file__ 等。第二个是我们创建了 message 对象后的结果,我们可以看到末尾增加了一个新的键值对,正好是我们变量和对应的对象。最后一个,在我们修改了 message 值以后,命名空间中的值也改变了。这就是我前面说的“对一个对象的读写操作,实际上就是在改变这个对象所处的命名空间的值 ”。

globals 是模块级别的命名空间,即我们直接在 py文件中创建的对象都会在这里面,而函数中的对象就不处在这里面,如下。后面再介绍其他的命名空间。



二、Python 中有哪些命名空间?

Python 目前(3.6.6)为止有四种命名空间,每种命名空间都有自己的一个 dict用来储存当前命名空间中的变量与对象的关系。

locals:本地命名空间,有的地方会解释为函数内部的命名空间,但是我们在函数外,调用 locals() 返回的却是当前所处位置的命名空间,也就是模块的命名空间,所以我认为,称为本地命名空间更合理。当在函数内部的时候,一般包括函数的局部变量以及形参。

enclosingfunction :嵌套函数中外部函数的命名空间,假设 out 里面嵌套了 inner 函数,那么 out 函数的 locals 就是 inner 函数的 enclosingfunction

globals: 当前模块的命名空间,所有在此模块中创建的和从其他模块导入的都会在这里面。

__builtins__:内置模块空间,Python 框架(Python 解释器自身),内置对象所处的命名空间。
 

三、命名空间的作用

命名空间有两个显而易见的作用,其一是取值,其二为赋值。取值的时候,Python 解释器会从当前的命名空间开始搜索,也就是 locals 命名空间,如果找不到就搜索其他的命名空间,搜索顺序是 LEGB

locals > enclosingfunction > globals > __builtins__

当查到到了以后,就停止搜索,并把变量加入到当前的命名空间,如果最终没有搜索到,那么就会抛出 NameError异常。记住我说的“当查到到了以后,就停止搜索,并把变量加入到当前的命名空间”,这句话很重要。变量的赋值操作是一个挺有意思的操作,变量的赋值,会是修改当前所处命名空间中对应的键值对,如果当前命名空间中没有这个键值对,那么就会重新新建一个键值对,如果外层的命名空间存在这个键值对,还是会在当前命名空间新键一个键值对,而且两个命名空间互不影响。

下面来看一个案例:


从上面的案例我们知道,函数内部的赋值操作,实际上会修改当前命名空间中的键值对或者新建。而且不影响外部命名空间的键值对。这意味着,把变量加入到当前的命名空间后,对变量的赋值,只对当前命名空间有效,变量指向的对象,也只在当前命名空间有效。

那么我们再来看一个例子
 


  1. # @Author   : stefanlei

  2. # @File     : main.py

  3.  

  4. WeChat_name = "Fluent-Python"

  5.  

  6.  

  7. def get_name():

  8.    print(locals())

  9.    print(WeChat_name)   # ?

  10.    WeChat_name = "New Name"

  11.    return WeChat_name

  12. get_name()


我们来思考一下,上面的代码输出是什么?

答案揭晓
 


  1. >> {}

  2. Traceback (most recent call last):

  3.  File "F:/FluentPython/main.py", line 15, in

  4.    print(get_name())

  5.  File "F:/FluentPython/main.py", line 9, in get_name

  6.    print(WeChat_name)

  7. UnboundLocalError: local variable 'WeChat_name' referenced before assignment

这里奇怪了,为什么 print(WeChat_namn) 会出错呢,异常的意思是:本地变量 WeChat_name在定义前使用。我不是在外面已经定义了吗?为什么还会这样?
 

这和命名空间仍然有关,同时这也是 Python 的运行机制导致的。可能有人说,Python 是解释性语言和 JavaScript 一样,实际上 Python 的执行过程也是需要编译的,在这个过程中,会有语法分析,然后是编译,最后是执行。在语法分析和编译阶段会 Python 解释器会确定函数中的变量是处在哪个命名空间中,并编译成字节码,后面再运行的过程中,Python 解释器就根据字节码来寻找相应的变量。

这样可能有点抽象,下面我们用 dis 模块来看看字节码是什么样的。


上面就是 get_name 对应的字节码,整个过程就是,Python 语法分析,检查当前函数中是否存在 WeChat_name 变量,如果有就在编译阶段把变量当做 locals 变量来处理,对应着 LOAD_FAST。
 

我们看到在编译阶段 WeChat_name 已经被 LOAD_FAST ,就是被编译为 locals 命名空间中的变量,在运行阶段,当 Python 解释器遇到 print(WeChat_name) 会尝试从 locals 命名空间中找相应的的变量(因为编译阶段把这个变量当作 locals 了),但是解释器发现, WeChat_name 变量存在,但是还没有赋值,所以才会抛出异常。这和 UnboundLocalError:localvariable'WeChat_name'referenced before assignment 这种异常是有区别的。
 

四、import 与命名空间

我们再回到最开始的案例,为什么通过 from...import... 导入的变量在经过修改以后,其他模块中再次使用的时候,变量没有被修改,而是最初的初始值。这其实就是由于使用了 from...import... 而不是 import ,我们在使用 from...import... 的时候,是直接把模块中的对象导入到当前的模块中,也就是把变量加入一个新的命名空间中,那我们在修改这变量的时候,实际上就是修改当前命名空间中对应的键值对,不影响其他命名空间空间的值,所以其他模块在使用这个变量的时候,还是初始值。
 

如果我们这个共享变量是一个 list 呢?假如有一个 list类型的变量 mylist ,这个时候如果直接 mylist=new 这样的效果和最开始的代码是一样,一个模块中变量的修改是不会影响另一个模块中这个变量的值的。但是如果是 mylist.append('new') 这样呢?这样就会影响其他模块中的值,换句话说,这样就可以达到共享变量的目的,为什么呢?
 

因为这样并没有修改命名空间中键值对,这样修改的仅仅是 mylist 对象中的值,模块命名空间中的键值对还是 mylist 。
 

难道我们每次共享变量只能用 list 类型吗?当然不是,我们可以使用 import... 方式来导入对象。

我们使用 import... 方式来导入的时候,我们导入的是一个模块,如果我们使用里面的对象,肯定是使用诸如 module.object 的方式来引用对象,这样引用方式的对象是不会出现在命名空间中的,而模块对象本身,是会出现在命名空间中,所以说假如我们用 module.object='new' 方式修改对象的值,是可以影响全局的,是可以实现真正的共享变量的,因为我们没有修改命名空间,这好比是对列表的元素进行修改,只不过这次的容器是模块,之前的是列表而已。

转载自:stefanlei Fluent Python

分享:0

电话咨询

客服热线服务时间

周一至周五 9:00-21:00

周六至周日 9:00-18:00

咨询电话

021-67690939
15201841284

微信扫一扫