资讯专栏INFORMATION COLUMN

由一个例子到python的名字空间

XiNGRZ / 2256人阅读

摘要:当程序引用某个变量的名字时,就会从当前名字空间开始搜索。对于可以看出已经被导入到自己的名字空间了,而不是在里面。因此并没有涉及到修改名字空间。按照原则,搜到有变量并且是个然后将其加入到自己的后面的就开始读取的元素,并没有影响的名字空间。

源自我的博客

前言

python里面最核心的内容就是:名字空间(namespace)


例子引入

例1

#!/usr/bin/env python
# encoding: utf-8


def func1():
    x = 1
    print globals()
    print "before func1:", locals()

    def func2():
        a = 1
        print "before fun2:", locals()
        a += x
        print "after fun2:", locals()

    func2()
    print "after func1:", locals()
    print globals()

if __name__ == "__main__":
    func1()

可以正常输出结果: 并且需要注意,在func2使用x变量之前的名字空间就已经有了"x":1.

before func1: {"x": 1}
before fun2: {"a": 1, "x": 1}
after fun2: {"a": 2, "x": 1}
after func1: {"x": 1, "func2": }

稍微改一点:如下

例2:

#!/usr/bin/env python
# encoding: utf-8


def func1():
    x = 1
    print "before func1:", locals()

    def func2():
        print "before fun2:", locals()
        x += x #就是这里使用x其余地方不变
        print "after fun2:", locals()

    func2()
    print "after func1:", locals()

if __name__ == "__main__":
    func1()

输出就开始报错: 而且在before func2也没有了x.

before func1: {"x": 1}
before fun2: {}
Traceback (most recent call last):
  File "test.py", line 18, in 
    func1()
  File "test.py", line 14, in func1
    func2()
  File "test.py", line 11, in func2
    x += x
UnboundLocalError: local variable "x" referenced before assignment

这两个例子正好涉及到了python里面最核心的内容:名字空间,正好总结一下,然后在解释这几个例子。


名字空间(Namespace)

比如我们定义一个"变量"

In [14]: a
NameError: name "a" is not defined

所以,这里更准确的叫法应该是名字。 一些语言中比如c,c++,java 变量名是内存地址别名, 而Python 的名字就是一个字符串,它与所指向的目标对象关联构成名字空间里面的一个键值对{name: object},因此可以这么说,python的名字空间就是一个字典.。

分类

python里面有很多名字空间,每个地方都有自己的名字空间,互不干扰,不同空间中的两个相同名字的变量之间没有任何联系一般有4种: LEGB四种

locals: 函数内部的名字空间,一般包括函数的局部变量以及形式参数

enclosiing function: 在嵌套函数中外部函数的名字空间, 对fun2来说, fun1的名字空间就是。

globals: 当前的模块空间,模块就是一些py文件。也就是说,globals()类似全局变量。

__builtins__: 内置模块空间, 也就是内置变量或者内置函数的名字空间。

当程序引用某个变量的名字时,就会从当前名字空间开始搜索。搜索顺序规则便是: LEGB.

locals  -> enclosing function -> globals -> __builtins

一层一层的查找,找到了之后,便停止搜索,如果最后没有找到,则抛出在NameError的异常。这里暂时先不讨论赋值操作。
比如例1中的a = x + 1 这行代码,需要引用x, 则按照LEGB的顺序查找,locals()也就是func2的名字空间没有,进而开始E,也就是func1,里面有,找到了,停止搜索,还有后续工作,就是把x也加到自己的名字空间,这也是为什么fun2的名字空间里面也有x的原因。

访问方式

其实上面都已经说了,这里暂时简单列一下

使用locals()访问局部命名空间

使用globals()访问全局命名空间
这里有一点需要注意,就是涉及到了from A import B 和import A的一点区别。

#!/usr/bin/env python
# encoding: utf-8

import copy
from copy import deepcopy


def func():
    x = 123
    print "func locals:",locals()

s = "hello world"
if __name__ == "__main__":
    func()
    print "globals:", globals()

输出结果:

func locals: {"x": 123}
globals: {"__builtins__": ,
 "__file__": "test.py",
 "__package__": None, 
"s": "hello world",
"func": ,
 "deepcopy": , 
"__name__": "__main__",
 "copy": , 
"__doc__": None}

从输出结果可以看出globals()包含了定义的函数,变量等。对于 "deepcopy": 可以看出deepcopy已经被导入到自己的名字空间了,而不是在copy里面。 而导入的import copy则还保留着自身的名字空间。因此要访问copy的方法,就需要使用copy.function了。这也就是为什么推荐使用import module的原因,因为from A import B这样会把B引入自身的名字空间,容易发生覆盖或者说污染。

生存周期

每个名字空间都有自己的生存周期,如下:

__builtins__: 在python解释器启动的时候,便已经创建,直到退出

globals: 在模块定义被读入时创建,通常也一直保存到解释器退出。

locals : 在函数调用时创建, 直到函数返回,或者抛出异常之后,销毁。 另外递归函数每一次均有自己的名字空间。

看着没有问题,但是有很多地方需要考虑。比如名字空间都是在代码编译时期确定的,而不是执行期间。这个也就可以解释为什么在例1中,before func2:的locals()里面包含了x: 1 这一项。再看下面这个,

def func():
    if False:
        x = 10 #该语句永远不执行
    print x

肯定会报错的,但是错误不是

NameError: global name "x" is not defined

而是:

UnboundLocalError: local variable "x" referenced before assignment

虽然x = 10永远不会执行,但是在执行之前的编译阶段,就会把x作为locals变量,但是后面编译到print的时候,发现没有赋值,因此直接抛出异常,locals()里面便不会有x。这个就跟例子2中,before func2里面没有x是一个道理。

赋值

为什么要把赋值多带带列出来呢,因为赋值操作对名字空间的影响很大,而且很多地方需要注意。
核心就是: 赋值修改的是命名空间,而不是对象, 比如:

a = 10

这个语句就是把a放入到了对应的命名空间, 然后让它指向一个值为10的整数对象。

a = []
a.append(1)

这个就是把a放入到名字空间,然后指向一个列表对象, 然而后面的a.append(1)这句话只是修改了list的内容,并没有修改它的内存地址。因此
并没有涉及到修改名字空间。
赋值操作有个特点就是: 赋值操作总是在最里层的作用域.也就说,只要编译到了有赋值操作,就会在当前名字空间内新创建一个名字,然后开始才绑定对象。即便该名字已存在于赋值语句发生的上一层作用域中;

总结 分析例子

现在再看例子2, 就清晰多了, x += x 编译到这里时,发现了赋值语句,于是准备把x新加入最内层名字空间也就是func2中,即使上层函数已经存在了; 但是赋值的时候,又要用到x的值, 然后就会报错:

UnboundLocalError: local variable "x" referenced before assignment

这样看起来好像就是 内部函数只可以读取外部函数的变量,而不能做修改,其实本质还是因为赋值涉及到了新建locals()的名字。
在稍微改一点:

#!/usr/bin/env python
# encoding: utf-8

def func1():
    x = [1,2]
    print "before func1:", locals()

    def func2():
        print "before fun2:", locals()
        x[0] += x[0] #就是这里使用x[0]其余地方不变
        print "after fun2:", locals()

    func2()
    print "after func1:", locals()

if __name__ == "__main__":
    func1()

这个结果就是:

before func1: {"x": [1, 2]}
before fun2: {"x": [1, 2]}
after fun2: {"x": [2, 2]}
after func1: {"x": [2, 2], "func2": }

咋正确了呢---这不应该要报错吗? 其实不然,就跟上面的a.append(1)是一个道理。
x[0] += x[0] 这个并不是对x的赋值操作。按照LEGB原则, 搜到func1有变量x并且是个list, 然后将其加入到自己的locals(), 后面的x[0] += x[0], 就开始读取x的元素,并没有影响func2的名字空间。另外无论func1func2的名字空间的x 没有什么关系,只不过都是对[1, 2]这个列表对象的一个引用。
这个例子其实也给了我们一个启发,我们知道内部函数无法直接修改外部函数的变量值,如例2,如果借助list的话, 就可以了吧!比如把想要修改的变量塞到一个list里面,然后在内部函数里面做改变!当然python3.x里面有了nonlocal关键字,直接声明一下就可以修改了。看到这里,对作用域理解应该有一点点了吧。

延伸 与闭包的不同

我们都知道闭包是把外部函数的值放到func.func_closure里面,为什么不像上面的例子一样直接放到函数的名字空间呢?
这是因为locals()空间是在函数调用的时候才创建! 而闭包只是返回了一个函数, 并没有调用,也就没有所谓的空间。

locals()与globals()

在最外层的模块空间里locals()就是globals()

In [2]: locals() is globals()
Out[2]: True

另外我们可以手动修改globals()来创建名字

In [3]: globals()["a"] = "abcde"
In [4]: a
Out[4]: "abcde"

但是locals()在函数里面的话, 貌似是不起作用的,如下:

In [5]: def func():
   ...:     x = 10
   ...:     print x
   ...:     print locals()
   ...:     locals()["x"] = 20
   ...:     print x
In [6]: func()
10
{"x": 10}
10

这是因为解释器会将 locals 名字复制到 一个叫FAST的 区域来优化访问速度,而实际上解释器访问对象时,是从FAST区域里面读取的,而非locals()。所以直接修改locals()并不能影响x(可以使用exec 动态访问,不再细述)。 不过赋值操作,会同时刷新locals()FAST区域。


查了不少资料,说了这么多,我只想说,作为python最核心的东西,名字空间还有很多很多地方需要探究,比如

作用域(scope)与名字空间, 这里只是模糊了二者的区别

面向对象,也就是类的名字空间, 又有不一样的地方。。。

学一点记录一点吧。

参考

1
2
3

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/37779.html

相关文章

  • Python与家国天下

    摘要:正如儒家经典所阐述修身齐家治国平天下。除此之外,模块还有如下最基本的属性在一个模块的全局空间里,有些属性是全局起作用的,称之为全局变量,而其它在局部起作用的属性,会被称为局部变量。 导读:Python猫是一只喵星来客,它爱地球的一切,特别爱优雅而无所不能的 Python。我是它的人类朋友豌豆花下猫,被授权润色与发表它的文章。如果你是第一次看到这个系列文章,那我强烈建议,请先看看它写的前...

    姘搁『 评论0 收藏0
  • Python与家国天下

    摘要:正如儒家经典所阐述修身齐家治国平天下。除此之外,模块还有如下最基本的属性在一个模块的全局空间里,有些属性是全局起作用的,称之为全局变量,而其它在局部起作用的属性,会被称为局部变量。 导读:Python猫是一只喵星来客,它爱地球的一切,特别爱优雅而无所不能的 Python。我是它的人类朋友豌豆花下猫,被授权润色与发表它的文章。如果你是第一次看到这个系列文章,那我强烈建议,请先看看它写的前...

    felix0913 评论0 收藏0
  • python基础知识之函数初阶——命名空间

    摘要:在内置命名空间不能使用全局和局部的名字。可以形象地理解成内置命名空间具有最高级别,不需要定义就可以使用,全局命名空间次之,最低级是局部命名空间。 python中的命名空间分三种: 内置的命名空间,在启动解释器的时候自动加载进内存的各种名字所在的空间,比如print,input等不需要定义就可以使用的名字 全局命名空间,就是从上到下所有我们定义的变量名和函数名所在的空间,是在程序从上到下...

    dack 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<