Python 的命名空间

懒得扫全文的童鞋,能够直接跳到最后看总结。
咱们先从一个简单的栗子提及:python

栗子

a 文件中有变量 va 以及类 A,b 文件导入 aclass A ,并打印出 Asegmentfault

#a.py
va = ['dobi', 'a', 'dog']

print('a1', id(va))

class A():
    def __init__(self):
        pass

    def rtn(self):
        global va
        va.insert(1,'is')
        print('a3', id(va))
        return va

print('a2', va)


#b.py
from a import A

print('b', A)

执行 b 文件的结果为:闭包

Reloaded modules: a
a1 2407907960200
a2 ['dobi', 'a', 'dog']
b <class 'a.A'>

能够发现,虽然 b 只是导入了 a 中的 class A,但导入这个过程却执行了整个 a 文件,那么咱们是否可以在 b 中访问 a 中的全局变量 va 呢:app

print(va)
# NameError: name 'va' is not defined
print(a.va)
# NameError: name 'a' is not defined
print(b.va)
# NameError: name 'b' is not defined

尝试了各种调用方法,发现都没法正常访问 a 的全局变量 va,既然 b 的导入执行了整个 a 文件,甚至还打印出了 vaid 和值,又为何没法在 b 中调用 va 呢?ide

这个问题所涉及到的内容就是:命名空间。函数

但在开始正题以前,咱们须要阐明若干概念:ui

一些基本概念的澄清

对象

Python 一切皆对象,每一个对象都具备 一个ID、一个类型、一个值;对象一旦创建,ID 便不会改变,能够直观的认为 ID 就是对象在内存中的地址spa

a = [1, 2]
b = a
id(a)
# 2407907978632
id(b)
# 2407907978632
b[1] = 3
a
# [1, 3]

上例 a, b 共享了同一个 ID、同一个值、同一个类型。所以 a, b 表达的是同一个对象,但 a, b 又明显是不一样的,好比一个叫 'a' 一个叫 'b'...既然是同一个对象,为何又有不一样的名字呢?难道名字不是对象的属性?设计

标识符

事实确实如此,这是 Python 比较特殊一点:如同'a' 'b' 这样的名称其实有一个共同的名字:identifier(注意不要与 ID 混淆了),中文名为“标识符”,来解释一下:code

标识符:各种对象的名称,好比函数名、方法名、类名,变量名、常量名等。

在 Python 中赋值并不会直接复制数据,而只是将名称绑定到对象,对象自己是不知道也不须要关心(该关心这个的是程序猿)本身叫什么名字的。一个对象甚至能够指向不一样的标识符,上例中的'a' 'b'即是如此。真正管理这些名子的事物就是本文的主角“命名空间”。

命名空间

命名空间(Namespace):名字(标识符)到对象的映射。

简而言之,命名空间能够理解为:记录对象和对象名字对应关系的空间;现今 Python 的大部分命名空间是经过字典来实现的,也即一个命名空间就是名字到对象的映射,标识符是键,对象则是值。

做用域

与命名空间相对的一个概念就是“做用域”,那么什么又是做用域呢?

做用域(Scope):本质是一块文本区域,Python 经过该文本区域能够直接访问相应的命名空间。

这里须要搞清楚什么是直接访问:

#x.py
a = 1
class A():
    def func():pass
python x.py
a   #直接访问
# 1
A.func  #属性访问

Python 中不加 . 的访问为直接访问,反之为属性访问。

所以能够简单的将做用域理解为“直接访问命名空间的一种实现”,具体而言:

  1. 做用域内相应的命名空间能够被直接访问;

  2. 只有做用域内的命名空间才能够被直接访问(所以并非全部的命名空间均可以被直接访问)。

看不懂? 不要紧,后面会解释,如今先回到命名空间这个话题上,咱们常常接触的命名空间有四类:

LEGB

LEGB 命名空间

这四类命名空间能够简记为 LEGB:

  1. 局部命名空间(local):指的是一个函数或者一个类所定义的名称空间;包括函数的参数、局部变量、类的属性等。

  2. 闭包命名空间(enclosing function):闭包函数 的名称空间(Python 3 引入)。

  3. 全局命名空间(global):读入一个模块(也即一个.py文档)后产生的名称空间。

  4. 内建命名空间(builtin):Python 解释器启动时自动载入__built__模块后所造成的名称空间;诸如 str/list/dict...等内置对象的名称就处于这里。

为了说清楚这几类命名空间,举个栗子:

#c.py
v1 = 'a global var'

def func(v):
    v2 = 'a local var'
    def inn_func():
        v3 = v2 + v
        return v3
    return inn_func

内建命名空间比较好理解,咱们重点讲解下其余三个:

  1. 'v1' 为全局变量 v1 的名子,其所处的命名空间为全局命名空间;须要注意的是全局命名空间包括 'func' 但不包括 func 的参数和内部变量。

  2. func 囊括 'v''v2''inn_func' 名称的空间为局部命名空间;

  3. 执行 func 后,func 的做用域释放(或许遗忘更合适),并返回了绑定了 vv2 变量的闭包函数 inn_func,此时闭包所具备命名空间即为闭包命名空间,所以局部命名空间和闭包命名空间是相对而言的,对于父函数 func 而言,二者具备产生时间上的差别。

LEGB 访问规则

经过上面描述,咱们发现 LEGB 四类命名空间自己具备明显的内外层级概念,而这种层级概念正是构建做用域的前提:做用域依据这种层级概念将不一样类型的命名空间组织起来并划归到不一样层级的做用域,而后定义好不一样层级做用域之间的访问规则,从而实现命名空间的直接访问:
LEGB 访问规则: 一样的标识符在各层命名空间中能够被重复使用而不会发生冲突,但 Python 寻找一个标识符的过程老是从当前层开始逐层往上找,直到首次找到这个标识符为止

#d.py
v1 = 1
v2 = 3
def f():
    v1 = 2
    print(1, v1)
    print(2, v2)

f()
print(3, v1)
1 2
2 3
3 1

上例中,全局变量和函数 f 都定义了 变量 v1,结果 Python 会优先选择 f 的局部变量 v1 ,对于 f 内并未定义的变量 v2 ,Python 会向上搜寻全局命名空间,读取全局变量 v2 后打印输出。

global 和 nonlocal 语句

global 和 nonlocal 的做用

如前所述,对于上层变量,python 容许直接读取,可是却不能够在内层做用域直接改写上层变量,来看一个典型的闭包结构:

#e.py
gv = ['a', 'global', 'var']

def func(v):
    gv = ['gv'] + gv #UnboundLocalError:local variable 'gv' referenced before assignment
    lv = []
    def inn_func():
        lv = lv + [v]  #UnboundLocalError:local variable 'lv' referenced before assignment
        gv.insert(1, lv[0])
        return gv
    return inn_func

实际调用 func()函数后,上面两处对 gvlv 进行赋值操做的地方都会发生 UnboundLocalError:由于 Python 在执行函数前,会首先生成各层命名空间和做用域,所以 Python 在执行赋值前会将func 内的 'gv' 'lv' 写入局部命名空间和闭包命名空间,当 Python 执行赋值时会在局部做用域、闭包做用域内发现局部命名空间和闭包命名空间内已经具备'gv''lv' 标识符,但这两个非全局标识符在该赋值语句执行以前并无被赋值,也即没有对象与标识符关联,所以没法参与四则运算,从而引起错误;但这段程序本意可能只是想让具备对象的全局变量gv 和局部变量 lv 参与运算,为了不相似的状况发生,Python 便引入了 globalnonlocal 语句就来讲明所修饰的 gvlv 分别来自全局命名空间和局部命名空间,声明以后,就能够在 funcinn_func 内直接改写上层命名空间内 gvlv 的值:

#f.py
gv = ['a', 'global', 'var']

def func(v):
    global gv
    gv = ['gv'] + gv
    lv = []
    print(id(lv))
    def inn_func():
        nonlocal lv
        lv = lv + [v]
        print(id(lv))
        gv.insert(1, lv[0])
        return gv
    return inn_func
a = func('is')
# 2608229974344

a()
# 2608229974344
# ['gv', 'is', 'a', 'global', 'var']

print(gv)
# ['gv', 'is', 'a', 'global', 'var']

如上,全局变量 gv 值被函数改写了, inn_func 修改的也确实是父函数 lv的值 (依据 ID 判断)。

借壳

那么是否是不使用 globalnonlocal 就不能达到上面的目的呢?来看看这段程序:

#g.py
gv = ['a', 'global', 'var']

def func(v):
    gv.insert(0, 'gv')
    lv = []
    print(id(lv))
    def inn_func():
        lv.append(v)
        print(id(lv))
        gv.insert(1, lv[0])
        return gv
    return inn_func

执行的结果:

a = func('is')
# 2608110869168

a()
# 2608110869168
# ['gv', 'is', 'a', 'global', 'var']

print(gv)
# ['gv', 'is', 'a', 'global', 'var']

能够发现,执行结果同上面彻底一致,问题天然来了:“为何不用 global nonlocal 也能够改写全局变量gv和父函数变量lv的值?

为了看清楚这个过程,咱们将上面的gv.insert(0, 'gv') lv.append(v) 改写为 gv[0:0] = ['gv'] lv[:] = [v]:

#h.py
gv = ['a', 'global', 'var']

def func(v):
    gv[0:0] = ['gv']
    lv = []
    print(id(lv))
    def inn_func():
        lv[:] = [v]
        print(id(lv))
        gv.insert(1, lv[0])
        return gv
    return inn_func

执行结果:

a = func('is')
# 2608229959496

a()
# 2608229959496
# ['gv', 'is', 'a', 'global', 'var']

同 g.py 文件的执行结果彻底一致,事实上二者之间的内在也是彻底同样的。
So 咱们其实改写的不是 gvlv ,而是 gvlv 的元素 gv[0:0]lv[:] 。所以,不须要 globalnonlocal 修饰就能够直接改写,这就是“借壳”,nonlocal 还没有引入 Python 中,好比 Python 2.x 若要在子函数中改写父函数变量的值就得经过这种方法。
固然借壳蕴藏着一个相对复杂的标识符建立的问题:好比子函数经过借壳修改父函数变量lv的值,那么子函数的标识符lv是怎么绑定到父函数变量lv的值 ID 的上的?

关于这个问题,这里有个问答就是讨论这个的:python的嵌套函数中局部做用域问题?

global 和 nonlocal 语句对标识符建立的不一样影响

另外,须要注意的是:global 语句只是声明该标识符引用的变量来自于全局变量,但并不能直接在当前层建立该标识符;nonlocal 语句则会在子函数命名空间中建立与父函数变量同名的标识符:

#j.py
gv = 'a global var'

def func():
    global gv
    lv = 'a local var'
    print(locals())
    def inn_func():
        nonlocal lv
        global gv
        print(locals())
    return inn_func

执行结果:

c = func()
{'lv': 'a local var'}   #运行 `func` 函数后,`global` 语句并未将 `gv` 变量引入局部命名空间

c()
{'lv': 'a local var'}   #运行闭包函数后,`nonlocal` 语句将父函数变量 `lv` 引入闭包命名空间

之因此 nonlocal 语句与 global 语句的处置不一样,在于全局变量的做用域生存期很长,在模块内随时均可以访问,而父函数的局部做用域在父函数执行完毕后便会直接释放,所以 nonlocal 语句必须将父函数变量的标识符和引用写入闭包命名空间。

命名空间的生命周期

建立规则

实际上,到这里其实还有一个重要的重要问题没有解决:“标识符并非天生就在命名空间内的,命名空间也不是无缘无故就产生的,那么命名空间是在何时被建立?又是在何时被删除的呢?”
规则有四:

  1. 内建命名空间在 Python 解释器启动时建立,以后会一直存在;

  2. 模块的全局命名空间在模块定义被读入时建立,一般模块命名空间也会保持到解释器退出。

  3. 函数调用时产生新的局部命名空间;函数返回结果、抛出异常时释放命名空间,每一次递归都生成一个命名空间。

  4. 标识符产生地点决定标识符所处的命名空间。

这四点就是拿来秒懂的!不过,仍然有一点经常被忽视:类的命名空间:

类的局部命名空间

首先,函数和类执行时都会产生局部命名空间,但类的执行机制不一样于函数:

#i.py
def a():
    print('function')

class A():
    print(1)
    class B():
        print(2)
        class C():
            print(3)

执行文件,结果为:

1
2
3

如上,类就是一个可执行的代码块,只要该类被加载,就会被执行,这一点不一样于函数。
类之因此这么设计的缘由在于:类是建立其余实例(生成其余的类或者具体的对象)的对象,所以必须在实例以前被建立,而类又可能涉及到与其余类的继承、重载等一系列问题,故在代码加载时就被建立利于提升效率和下降逻辑复杂度。

其次,与函数不一样的是,类的局部命名空间并不是做用域

class A():
    a = 1
    b = [a + i for i in range(3)]  #NameError: name 'a' is not defined

执行上段代码,咱们能够发如今类 A 内列表推导式没法调取 a 的值,但函数却能够:

def func():
    a = 1
    b = [a + i for i in range(3)]
    print(b)

func()  #[1, 2, 3]

所以,A 中的 a 不一样于函数 func 中的 a 在局部命名空间中能够被任意读取,之因此说是“不能够被任意”读取而不是“不可被读取”,缘由在于在类A 的局部空间内,a 其实必定程度上是能够直接被读取的:

class A():
    a = 1
    c = a + 2

执行上段代码后:

A.c 
#3

而上例中 b 的赋值操做不能执行,缘由在于列表推导式会建立本身的局部命名空间,所以难以访问到 a

编译与局部命名空间

Python 是动态语言,不少行为是动态发生的,但 Python 自身也在不断进步,好比为了提升效率,有些行为会在编译时候完成,局部变量的建立就是如此:

def func():
    a = 1
    def inn_func():
        print(a)  # error
        a = 2     # error
    inn_func()

上段程序还未执行,就提示存在有语法错误,缘由在于python 解释器发现 inn_func 内存在自身的 a 变量,但却在声明以前就被 print 了。

总结

啰嗦了这么多,终于该结尾了!
咱们再来回过头来看下文章开头的栗子:
一、为何 b.py 只是导入 a.py 中的 class A,却执行了整个 a.py 文件?
答:由于 Python 并不知道 class A 在 a.py 文档的何处,为了可以找到 class A,Python 须要执行整个文档。
二、为何 b.py 的导入执行了整个 a.py 文档,却在 b 中难以调用 a 的全局变量 va
答:Python 的全局变量指的是模块全局,所以不能够跨文档,所以 global 语句也是不能够跨文档的。另外, b 只是导入了 a 的 class A,所以并不会导入 a 中全部的标识符,因此 相似a.va 这样的调用也是不起做用的。

关于命名空间:
一、赋值、定义类和函数都会产生新的标识符;
二、全局变量的标识符不能跨文档;
三、各级命名空间相互独立互不影响;
四、Python 老是从当前层逐渐向上寻找标识符;
五、内层做用域若想直接修改上层变量,须要经过 global nonlocal 语句先声明;
六、单纯的 global 语句并不能为所在层级建立相应标识符,但 nonlocal 语句能够在闭包空间中建立相应标识符;七、类的局部命名空间不是做用域。

相关文章
相关标签/搜索