I used to work for the government. Now I work for the public.

容易忽视的命名空间

前言:最近在写一个程序的时候需要用到一个全局共享的变量。最初使用的时候,我的思路如下,把全局变量定义在当前模块当中,

# @Author   : stefanlei
# @File     : app.py


globa_data = 0

def f1():
    globa_data = 1
    pass

def f2():
    print(globa_data)
    pass

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

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

代码如下:

# @Author   : stefanlei
# @File     : initialize.py

globa_data = 0
print("默认值为",globa_data)
# @Author   : stefanlei
# @File     : app1.py
from initialize import globa_data

def f1():
    # 修改了 globa_data 的值为 1
    globa_data = 1
    print('当前值为',globa_data)

# @Author   : stefanlei
# @File     : app2.py
from initialize import globa_data

def f2():
    print('当前值为',globa_data)

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

# @Author   : stefanlei
# @File     : main.py

from app1 import f1
from app2 import f2

f1()
f2()

# 输出结果
# 默认值为 0
# f1 当前值为 1
# f2 当前值为 0

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

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

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

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

# @Author   : stefanlei
# @File     : main.py

print(globals())
message = "Fluent Python"
print(globals())
message = "流畅的 Python"
print(globals())
>> {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002282012D240>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'F:/FluentPython/main.py', '__cached__': None}

>> {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002282012D240>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'F:/FluentPython/main.py', '__cached__': None, 'message': 'Fluent Python'}

>> {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002282012D240>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'F:/FluentPython/main.py', '__cached__': None, 'message': '流畅的 Python'}

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

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

# @Author   : stefanlei
# @File     : main.py

# 这个处在globals命名空间中
WeChat_id = "Fluent-Python"

# 这个函数也处在globals命名空间中
def get_name():
    # 这个对象不处在globals命名空间中
    WeChat_name = "Fluent Python"
print(globals())
>> {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002551850D240>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'F:/项目/FluentPython/main.py', '__cached__': None, 'WeChat_id': 'Fluent-Python', 'get_name': <function get_name at 0x0000025516903E18>}

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

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

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

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

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

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

三、命名空间的作用

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

locals > enclosing function > globals > __builtins__

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

下面来看一个案例:

# @Author   : stefanlei
# @File     : main.py

WeChat_name = "Fluent-Python"

def get_name():
    print(locals())  # {}
    # 赋值操作是在当前命名空间中新建了一个键值对,与外界的互不影响。
    WeChat_name = "New Name"
    print(locals())  # {'WeChat_name': 'New Name'}
    return WeChat_name

print(get_name())    # New Name
print(locals())      
print(WeChat_name)   # Fluent-Python
>> 
{}  
{'WeChat_name': 'New Name'}
New Name
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x00000245BE9FD208>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'F:/项目/FluentPython/main.py', '__cached__': None, 'WeChat_name': 'Fluent-Python', 'get_name': <function get_name at 0x00000245BEACBB70>}
Fluent-Python

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

那么我们再来看一个例子

# @Author   : stefanlei
# @File     : main.py

WeChat_name = "Fluent-Python"


def get_name():
    print(locals())
    print(WeChat_name)   # ?
    WeChat_name = "New Name"
    return WeChat_name
get_name()

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

答案揭晓

>> {}
Traceback (most recent call last):
  File "F:/FluentPython/main.py", line 15, in <module>
    print(get_name())
  File "F:/FluentPython/main.py", line 9, in get_name
    print(WeChat_name)
UnboundLocalError: local variable 'WeChat_name' referenced before assignment

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

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

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

# @Author   : stefanlei
# @File     : main.py

from dis import dis

WeChat_name = "Fluent-Python"

def get_name():
    print(WeChat_name)
    WeChat_name = "New Name"
    return WeChat_name

dis(get_name)

 10           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (WeChat_name)
              4 CALL_FUNCTION            1
              6 POP_TOP

 11           8 LOAD_CONST               1 ('New Name')
             10 STORE_FAST               0 (WeChat_name)

 12          12 LOAD_FAST                0 (WeChat_name)
             14 RETURN_VALUE

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

我们看到在编译阶段 WeChat_name 已经被 LOAD_FAST ,就是被编译为 locals 命名空间中的变量,在运行阶段,当 Python 解释器遇到 print(WeChat_name) 会尝试从 locals 命名空间中找相应的的变量(因为编译阶段把这个变量当作 locals 了),但是解释器发现,WeChat_name 变量存在,但是还没有赋值,所以才会抛出异常。这和 UnboundLocalError: local variable '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' 方式修改对象的值,是可以影响全局的,是可以实现真正的共享变量的,因为我们没有修改命名空间,这好比是对列表的元素进行修改,只不过这次的容器是模块,之前的是列表而已。

关于命名空间大致就是这些内容了,如果还有其他的内容,欢迎加我微信投稿。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注