Python 的名称空间和作用域

最开始对名称空间的了解是在学习函数的时候,那时候知道了作用域的查找顺序,以及全局名称空间和局部名称空间,产生疑惑的时候为学递归的时候,那时候还没有名称空间这个概念,只知道递归有个最大深度,那时候以后递归产生的名称空间是一层套一层的,以及类里面的名称空间,所以产生了深究名称空间的想法,这才诞生了这篇博客,本篇博客借鉴了python的命名空间的内容,本人对里面的例子都试验了并发现了一个错误,在类中定义的列表生成式产生的命名空间也可以访问到类属性.

例子

# a.py
lis = ['musibii', 'thales']
print('lis1', id(name))

class A:
    def __init__(self):
        pass
    
    def func(self):
        global lis
        lis.append('maffia')
        print('lis', id(lis))
        return lis
    
print('lis2', lis)

# b.py
from a import A
print('b', A)

执行 b 文件的结果为:

lis1 4421146632
lis2 ['musibii', 'thales']
b <class 'a.A'>

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

使用 from a import A 的形式只是把 A 产生的名称空间复制了一份到 b 产生的名称空间中.

Python的一些基本概念

对象

在Python 中一切皆对象,那么对象到底代表什么呢?我的理解是在执行 py 文件时产生的一切变量都称为对象,如果把内存比作一座超大的房子的话,那么对象就是这座房子里的租客,那么这个租客随身携带的东西就是这个对象的一切了,对象都具有唯一的 id( 内存地址),类型( python3中统一了类和类型的概念),以及对象的值,对象一旦建立, id 便不会改变, id 就是对象在内存中的地址.

用常见的对象来类比一下这三个概念:

  1. 常量
NAME = 'musibii'
print(id(NAME))
print(type(NAME))
print(globals()) # 查看全局名称空间

# 运行结果
4374978776
<class 'str'>
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x104c45fd0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/Users/jingxing/PycharmProjects/python全栈/作业/day27/duixiang.py', '__cached__': None, 'NAME': 'musibii'}

出来的结果分别是 id,类型以及字典里面最后一个键值对对应的就是 NAME 的值.

  1. 函数
def func():
    name = 'musibii'
    def inner():
        age = 18
        print('age', age)
    inner()
    print(locals()) # 想要查看函数里面的值,必须在函数内部查看,因为函数执行完成会释放内存资源

print(id(func))
print(type(func))
func()

# 运行结果
4529634840
<class 'function'>
age 18
{'inner': <function func.<locals>.inner at 0x10e1cac80>, 'name': 'musibii'}

id 和类型不必多说,函数对应的属性值里面不包括嵌套函数里面的属性(说明:名称空间是相互独立的)

class Cls:
    school = 'hashangda'
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def tell_info(self):
        print('%s : %s' % (self.name, self.age))

print(id(Cls))
print(type(Cls))
print(Cla.__dict__)

# 运行结果
140390381504248
<class 'type'>
{'__module__': '__main__', 'school': 'hashangda', '__init__': <function Cls.__init__ at 0x10648eae8>, 'tell_info': <function Cls.tell_info at 0x10648ebf8>, '__dict__': <attribute '__dict__' of 'Cls' objects>, '__weakref__': <attribute '__weakref__' of 'Cls' objects>, '__doc__': None} 

类的 id 为内存地址,类型为type类型,在 type 里面 type 就是类型的意思,所以说 python 里所有的类的类型都是类型,而类里面的属性就是类的值了.

python 里面所有的对象都具有的并且根确定身份有关的值为 id, 类型和值了.名称不是对象的属性,名称只是指向对象,因为可以多个名称指向同一个对象.

标识符

在对象里把变量名叫为名称其实是不准确的,这些名称都有一个共同的名字: identifier(和 id 是两个意思),中文名为'标识符'.

标识符:在 Python中,各类对象的名称,比如函数名,方法名,类名,变量名.常量名都称为标识符.

在 Python 中赋值并不会直接复制数据,而只是将名称绑定到对象,对象本身不需要知道和关心自己的标识符叫什么,一个对象甚至可以指向不同的标识符.真正管理这些标识符的事物是'名称空间'.

名称空间

名称空间(NameSpace):名字(标识符)到对象的映射.

简单来说,名称空间可以理解为记录对象和对象名字对应关系的空间,在对象那里查看的对象的值就是名称空间,这是一个字典,一个命名空间就是名字到对象的映射,标识符是键,对象则是值.

作用域

与名称空间相对的一个概念就是'作用域',作用域本质是一块文本区域, Python 通过该文本区域可以直接访问相应的名称空间.

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

因此可以简单的将作用域理解为直接访问名称空间的一种实现,具体而言:

  1. 作用域内相应的名称空间可以被直接访问;
  2. 只有作用域内的名称空间才可以被直接访问(因此并不是所有的名称空间都可以被直接访问).

LEGB

LEGB 名称空间

这四类名称空间可以简记为 LEGB:

  1. 局部名称空间( local): 指的是一个函数或者一个类所定义的名称空间:包括函数的参数,局部变量,类的属性等;
  2. 闭包名称空间(enclosing function):闭包函数的名称空间( python3引入);
  3. 全局名称空间( global):读入一个模块(也即一个. py 文件)后产生的名称空间;
  4. 内建名称空间(builtin):Python 解释器启动时自动载入__builtin__ 模块后形成的名称空间;像 str/list/dict... 等内置对象的名称就处于这里.

举例:

# test.py
v1 = 'global var'

def func(v1):
    v2 = 'local var'
    def inner():
        v3 = v2 + v
        return v3
    return inner

内建的反正每次都一样,在这里的三个名称空间里面的名称为:

  1. 'v1' 为全局变量 v1 的名字,其所处的名称空间为全局名称空间;需要注意的是全局名称空间包括 'func' 但不包括 func 的参数和内部变量;
  2. func 包括 'v', 'v2', 和 'inner'名称的局部名称空间;
  3. 执行 func 后, func 的作用域释放,并返回绑定了 v 和 v2变量的闭包函数 inner, 此时闭包函数的名称空间即为闭包名称空间,因此局部名称空间和闭包名称空间是相对而言的,对于父函数 func 而言,两者具有产生时间上的差异.

LEGB 访问规则

通过上面的例子,发现 LEGB 四类名称空间本身具有明显的内外层级概念,而这种层级概念正是构建作用域的前提:作用域依据这种层级概念将不同类型的名称空间组织起来并划归到不同层级的作用域,然后定义好不同层级作用域之间的访问规则,从而实现名称空间的直接访问.

LEGB 的访问规则:**同样的标识符在各层名称空间可以被重复使用而不会发生冲突,但 Python 寻找一个标识符的过程总是从当前层开始逐层网上找,直到首次找到这个标识符为止.

# main.py
v1 = 1
v2 = 3
def func():
    v1 = 2
    print(v1)
    print(v2)
    
func()
print(v1)

# 运行结果
2
3
1

全局变量和函数 func 都定义了变量 v1,在函数内部会优先选择自己局部名称空间内的变量 v1,对于 func 中未定义的变量 v2,Python 会向上查找全局名称空间,读取全局变量后打印输出.

global 和 nonlocal 语句

global 和 nonlocal 的作用

对于上层名称空间里的变量, python 允许直接读取,但是不可以在内层作用域直接改写上层变量,在这方面很明显的区别就是在闭包函数里.

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

def func(v):
    gv = ['gv'] + gv
    lv = []
    def inner():
        lv = lv + [v]
        gv.insert(1, lv[0])
        return
    return inner

实际调用 func 函数后,上面两处对 gv 和 lv 进行赋值的操作都会发生UnboundLocalError:因为 python 在执行函数前,会首先生成各层名称空间和作用域,因此 python 会在执行赋值前将 func 内的 gv 和 lv 写入局部名称空间和闭包名称空间,当 python 执行赋值语句的时候,会发现在局部作用域,闭包作用域内发现局部名称空间和闭包名称空间内已经具有 gvlv 标识符,但是这两个非全局标识符在赋值语句执行之前并没有被赋值,也即没有对象与标识符关联,因此无法参与赋值运算,从而触发在引用之前未赋值的错误;但这段程序的本意是为了让全局变量 gv 和局部变量 lv 参与运算,为了避免类似的情况发生,Python 引入了globalnonlocal语句来说明局部名称空间和闭包名称空间使用的标识符分别来自全局名称空间和局部名称空间,声明之后就可以在 func 和 inner 名称空间里直接改写上层名称空间内的gvlv的值了.

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

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

这样就可以正常修改了.原博客说 lv 的 id 都是一样的,在 mac 上版本为3..6.6测试发现不是一样的.

# 运行结果
4416600136
4416615624
# 确实是使用了上层名称空间的变量,但是重新赋值后, gv 和上层的 gv 已经不是同一个了, lv 也一样,所以内存地址都发生变化了.

借壳

那么不用 global 和nonlocal 可以达到上面的目的吗?

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

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

看作者的这个解释为借壳,感觉很形象,这个原因应该用列表式可变类型来解释比较好, insert 和 append 的操作并没有对列表进行赋值操作,而是对列表这个容器里面的东西修改了并不是修改列表,我们都知道电脑内的内存永远都不会改变,他就是个超大的容器,用来装东西的,假如现在有个列表,它的容量和内存一样大,那么是不是好理解点,我们在和内存一样的列表里操作会改变这个列表吗?不会.

在 global 和 nonlocal 的作用就是把上层有的标识符和对象复制一份到下层名称空间

为什么在下层作用域可以使用读取上层作用域的值而不可以使用它的值呢?

这是因为 python 的两个概念不同的原因,作用域和名称空间的区别导致的.标识符的查找顺序是从当前层一层层向外查找的,这是根据作用域来查找的,那么名称空间呢?作用域和名称空间的在执行 py 文件就已经产生了,其实可以理解这是一个东西,其实里面保存的信息(除了某些可能有特殊含义的值不一样外)都一样,不一样的是作用于里面只保存了标识符,而名称空间里不仅保存了标识符还保存了标识符对应的对象.查找值的顺序是根据作用域来的,使用值就只能使用自己名称空间里的值,名称空间里保存的是标识符和标识符对应的对象,只有本层名称空间里有这个标识符和标识符对应的对象才可以使用(读,使用).

那么现在很明白为什么不能直接在下层名称空间直接使用上层名称空间里的名称了.

global 和 nonlocal 语句对标识符创建的不同影响

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

# j.py
gv = ['a global var']
def func():
    global
    lv = 'a local var'
    print(locals())
    def inner():
        nonlocal lv
        global gv
        print(locals())
    return inner

# 运行结果
{'lv': 'a local var'}
{'lv': 'a local var'}

运行 func 函数后 global 语句并未将 ' gv '变量引入局部名称空间,执行闭包函数后,' local'语句将父函数变量'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

# 运行结果
3

而上例中 b 的赋值操作不能被执行,原因在于列表推导式会创建自己的局部名称空间,因此难以访问到 a???????

为什么访问不到 a???

编译与局部名称空间

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

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

上段程序还未执行,就提示存在语法错误,原因在于 Python 解释器发现 inner 内存在自身的 a 变量,但在声明之前就被 print 了.

总结

1、为什么 b.py 只是导入 a.py 中的 class A,却执行了整个 a.py 文件?
答:因为 Python 并不知道 class A 在 a.py 文档的何处,为了能够找到 class A,Python 需要执行整个文档。
2、为什么 b.py 的导入执行了整个 a.py 文档,却在 b 中难以调用 a 的全局变量 va
答:Python 的全局变量指的是模块全局,因此不可以跨文档,因此 global 语句也是不可以跨文档的。另外, b 只是导入了 a 的 class A,因此并不会导入 a 中所有的标识符,所以 类似a.va 这样的调用也是不起作用的。

关于命名空间:
1、赋值、定义类和函数都会产生新的标识符;
2、全局变量的标识符不能跨文档;
3、各级命名空间相互独立互不影响;
4、Python 总是从当前层逐渐向上寻找标识符;
5、内层作用域若想直接修改上层变量,需要通过 global nonlocal 语句先声明;
6、单纯的 global 语句并不能为所在层级创建相应标识符,但 nonlocal 语句可以在闭包空间中创建相应标识符;
7、类的局部命名空间不是作用域。

猜你喜欢

转载自www.cnblogs.com/zuanzuan/p/9860996.html