2 命名空间
2.1 什么是命名空间
2.2 命名空间的类型
2.3 命名空间的生命周期
3 作用域
3.1 什么是作用域
3.2 命名空间的查找顺序
3.3 glocal 与 nonlocal
3.4 globals()和 locals()函数
4 易错情况
5 总结
命名空间与作用域是程序设计中的基础概念, 深入理解有助于理解变量的生命周期, 减少代码中的莫名其妙 bug.Python 的命名空间与作用域与 Java,C++ 等语言有很大差异, 若不注意, 就可能出现莫名其妙的问题.
2 命名空间
2.1 什么是命名空间
命名空间, 即 Namespace, 也成为名称空间或名字空间, 指的是从名字到对象的一个映射关系, 类似于字典中的键值对, 实际上, Python 中很多命名空间的实现用的就是字典.
不同命名空间是相互独立的, 没有任何关系的, 所以同一个命名空间中不能有重名, 但不同的命名空间是可以重名而没有任何影响.
2.2 命名空间的类型
Python 命名空间按照变量定义的位置, 可以划分为以下 3 类:
Built-in, 内置命名空间, python 自带的内建命名空间, 任何模块均可以访问, 存放着内置的函数和异常.
Global, 全局命名空间, 每个模块加载执行时创建的, 记录了模块中定义的变量, 包括模块中定义的函数, 类, 其他导入的模块, 模块级的变量与常量.
Local, 局部命名空间, 每个函数, 类所拥有的命名空间, 记录了函数, 类中定义的所有变量.
一个对象的属性集合, 也构成了一个命名空间. 但通常使用 objname.attrname 的间接方式访问属性, 而不是直接访问, 故不将其列入命名空间讨论.(直接访问: 直接使用名字访问的方式, 如 name, 这种方式尝试在名字空间中搜索名字 name. 间接访问: 使用形如 objname.attrname 的方式, 即属性引用, 这种方式不会在命名空间中搜索名字 attrname, 而是搜索名字 objname, 再访问其属性.)
2.3 命名空间的生命周期
不同类型的命名空间有不同的生命周期:
内置命名空间在 Python 解释器启动时创建, 解释器退出时销毁;
全局命名空间在模块被解释器读入时创建, 解释器退出时销毁;
局部命名空间, 这里要区分函数以及类定义. 函数的局部命名空间, 在函数调用时创建, 函数返回结果或抛出异常时被销毁 (每一个递归函数都拥有自己的命名空间); 类定义的命名空间, 在解释器读到类定义(class 关键字) 时创建, 类定义结束后销毁.(*)
3 作用域
3.1 什么是作用域
作用域是针对命名空间而言, 指命名空间在程序里的可应用范围, 或者说是 Python 程序 (文本) 的某一段或某几段, 在这些地方, 某个命名空间中的名字可以被直接引用. 这部分程序就是这个命名空间的作用域. 只有函数, 类, 模块会产生新的作用域, 代码块 (例如 if,for 代码块) 不会产生新的作用域.
另外, python 中变量的作用域是由它在源代码中的位置决定的 (*). 由一个赋值语句引进的名字在这个赋值语句所在的作用域里是可见(起作用) 的, 而且在其内部嵌套的每个作用域内也可见, 除非它被嵌套于内部的且引进同样名字的赋值语句所遮蔽.
3.2 命名空间的查找顺序
上述作用域的定义中表名了命名空间与作用于之间的关系: 作用于是命名空间的可见范围. 那么, 在程序中访问某个名称时, 是怎样一个搜索顺序呢? 按照 LEGB 顺序搜索:
Local: 首先搜索, 包含局部名字的最内层 (innermost) 作用域, 如函数 / 方法 / 类的内部局部作用域;
Enclosing: 根据嵌套层次从内到外搜索, 包含非局部 (nonlocal) 非全局 (nonglobal) 名字的任意封闭函数的作用域. 如两个嵌套的函数, 内层函数的作用域是局部作用域, 外层函数作用域就是内层函数的 Enclosing 作用域;
Global: 倒数第二次被搜索, 包含当前模块全局名字的作用域;
Built-in: 最后被搜索, 包含内建名字的最外层作用域.
Python 按照以上 LEGB 的顺序依次在四个作用域搜索名字, 没有搜索到时, Python 抛出 NameError 异常. 所以:
在局部作用域中, 可以看到局部作用域, 嵌套作用域, 全局作用域, 内建作用域中所有定义的变量.
在全局作用域中, 可以看到全局作用域, 内建作用域中的所有定义的变量, 无法看到局部作用域中的变量.
在 Python 中, 类定义所引入的作用域对于成员函数是不可见的, 这与 C++ 或者 Java 是很不同的, 因此在 Python 中, 成员函数想要引用类体定义的变量, 必须通过 self 或者类名来引用它.(我的理解是 Python 类中所有变量有一个作用域, 每个成员函数都有各自都作用域, 这些作用域都是 Local, 且是平级的 *)
用一个类比来理解命名空间与作用域:
四种作用域相当于我们生活中的国家(Built-in), 省(Global), 市(Enclosing), 县(Local), 命名空间相当于公务员花名册, 记录着哪个职位是哪个人. 国家级公务员服务于全国
民众 (全国老百姓都可以喊他办事), 省级公务员只服务于本身民众(国家层面的人或者其他省的人我不管), 市(Enclosing), 县(Local) 也是一个道理. 当我们要找某一类领导(例如想找
个警察帮我打架)时 (要访问某个名称), 如果我是在县(Local) 里头, 优先在县里的领导花名册中找(优先在自己作用域的命名空间中找), 县里花名册中没警察没有就去市里的花名册找(往
上一层作用域命名空间找), 知道找到国家级都还没找到, 那就会报错. 如果省级民众想找个警察帮忙大家, 不会找市里或者县里的, 只会找自己省里的(其它省都不行), 或者找国家级的. 国家,
省, 市, 县肯定一直都在那里, 可不会移动(作用域是静态的); 领导可以换届, 任期移到就换人(命名空间是动态的, 每次调用函数都会新的命名空间, 函数执行结束, 命名空间销毁).
3.3 glocal 与 nonlocal
当在一个函数内部为一个变量赋值时, 并不是按照上面所说 LEGB 规则来首先找到变量, 之后为该变量赋值. 在 Python 中, 在函数中为一个变量赋值时, 有下面这样一条规则:
"当在函数中给一个变量名赋值是(而不是在一个表达式中对其进行引用),Python 总是创建或改变本地作用域的变量名, 除非它已经在那个函数中被声明为全局变量."
那么, 若想要在函数中修改全局变量, 而不是在函数中新建一个变量, 此时便要用到关键字 global 了.
- i = 1
- def func():
- global i
- print(i) #输出 1
- i = 2
- func()
- print(i) #输出 2
关键字 nonlocal 的作用与关键字 global 类似, 使用 nonlocal 关键字可以在一个嵌套的函数中修改嵌套作用域中的变量, 示例如下:
- def f1():
- i = 1
- def f2():
- nonlocal i
- print(i) #输出 1
- i = 2
- f2()
- print(i)
- f1() #输出 2
第一, 两者的功能不同. global 关键字修饰变量后标识该变量是全局变量, 对该变量进行修改就是修改全局变量, 而 nonlocal 关键字修饰变量后标识该变量是上一级函数中的局部变量, 如果上一级函数中不存在该局部变量, nonlocal 位置会发生错误(最上层的函数使用 nonlocal 修饰变量必定会报错).
第二, 两者使用的范围不同. global 关键字可以用在任何地方, 包括最上层函数中和嵌套函数中, 即使之前未定义该变量, global 修饰后也可以直接使用, 而 nonlocal 关键字只能用于嵌套函数中, 并且外层函数中定义了相应的局部变量, 否则会发生错误.
对上面代码略作修改:
- i = 0
- def f1():
- i = 1
- def f2():
- global i #此处改为 glocal
- print(i) #输出 0
- i = 2
- f2()
- print(i)
- f1() #输出 2
3.4 globals()和 locals()函数
根据调用地方的不同, globals()和 locals()函数可被用来返回全局和局部命名空间里的名字.
如果在函数内部调用 locals(), 返回的是所有能在该函数里访问的命名.
如果在函数内部调用 globals(), 返回的是所有在该函数里能访问的全局名字.
两个函数的返回类型都是字典. 所以名字们能用 keys()函数摘取.
4 易错情况
上文介绍了变量名的搜索顺序是 LEGB 的, 其中 G,B 两个作用域的引入在不能够通过代码操作的, 能够通过语句引入的作用域只有 E 和 L.Python 中也只能函数和类的定义能引入新作用域. 另外, 在实际开发中, 一定要主要函数定义引入 local 作用域或者 Enclosing 作用域中对应命名空间的声明周期. 下面列举 Python 中的几例特殊情况. 如果你觉得已经理解并掌握了上面命名空间与作用于的知识, 请尝试解释下面的情况:
(1)情况 1:
- def test():
- i = 0
- test()
- print(i)
推测出输出结果了吗? 没错, 会报错: NameError: name 'i' is not defined. 切记: 函数的命名空间在函数被调用时创建, 函数执行完毕, 命名就也被销毁. 另外, LEGB 搜索法则也不会让全局作用域去局部作用域寻找.
(2)情况 2:
- if True:
- i = 1
- print(i) # 可以正常输出 i 的值 1, 不会报错
if 条件判断语句不会引入新的作用域, 所以, 语句 "i=1" 与 "print(i)" 属于同一作用域, 既然同属于一个作用域, 也不存在说 if 代码块运行完之后, 作用域销毁, 所以 i 一直存在, 可以正常执行.
(3)情况 3:
- for i in range(10):
- pass
- print(i) #输出结果是 9, 而不是 NameError
for 循环不会引入新的作用域, 所以, 循环结束后, 继续执行 print(i), 可以正常输出 i, 原理上与情况 3 中的 if 相似. 这一点 Python 就比较坑了, 因此写代码时切忌 for 循环名字要与其他名字不重名才行.
(4)情况 4
- list_1 = [i for i in range(5)]
- print(i)
情况 3 中说到过, for 循环不会引入新的朱用于, 那么为什么输出报错呢? 真相只有一个: 列表生成式会引入新的作用域, for 循环是在 Local 作用域里面的. 事实上, lambda, 生成器表达式, 列表解析式也是函数, 都会引入新作用域.
(5)情况 5:
- def import_sys():
- import sys
- import_sys()
- print(sys.path) # 报错: NameError: name 'sys' is not defined
在函数内部进行模块导入时, 导入的模块只在函数内部作用域生效. 这个算非正常程序员的写法了, import 语句在函数 import_sys 中将名字 sys 和对应模块绑定, 那 sys 这个名字还是定义在局部作用域, 跟上面的例子没有任务区别. 要时刻切记 Python 的名字, 对象, 这个其他编程语言不一样.
(6)情况 6:
只引用上层作用域中的值时:
- def test():
- print(i)# 可正常输出 0
- i = 0
- test()
在局部作用域中可以引用全局作用域中的命名空间.
注: 可不要认为 i=0 这行必须卸载 def test()前面, 事实上只需要在 test()函数调用前写 i=0 即可, 因为函数的命名空间是在函数被调用时创建的.
继续上面的例子, 若是对值进行修改:
- def test():
- print(i)
- i= 2
- i = 0
- test()
报错: UnboundLocalError: local variable 'i' referenced before assignment
Python 对局部作用域情有独钟, 解释器执行到 print(i),i 在局部作用域没有. 解释器尝试继续执行后面定义了名字 i, 解释器就认为代码在定义之前就是用了名字, 所以抛出了这个异常. 如果解释器解释完整个函数都没有找到名字 i, 那就会沿着搜索链 LEGB 往上找了, 最后找不到抛出 NameError 异常.
是不是觉得另有所悟, 对上面的代码稍作修改, 能否推测出结果:
- def test():
- i = [2 , 2]
- i = [1 , 2]
- test()
- print(i)
输出结果:
[1 , 2]
我想你应该猜到了结果, 这个和上面的例子基本是一样的. 再改一下:
- def test():
- i[0] = 2
- i = [1 , 2]
- test()
- print(i)
输出结果:
[2, 2]
猜到了吗? 是不是有些懵逼. list 作为一个可变对象, l[0] = 2 并不是对名字 l 的重绑定, 而是对 l 的第一个元素的重绑定, 所以没有新的名字被定义. 因此在函数中成功更新了全局作用于中 l 所引用对象的值.
(7)情况 7:
请对比下面几种示例代码:
第一种:
- i = 1
- def f1():
- print(i)
- def f2():
- i = 2
- f1()
- f2()
- print(i)
第二种:
- i = 1
- def f1():
- print(i)
- def f2():
- i = 2
- return f1
- ret = f2()
- ret()
- print(i)
第三种:
- i = 1
- def f1():
- i = 2
- def f2():
- print(i)
- return f2
- func = f1()
- func()
- print(i)
先别看答案, 想想输出结果!
第一种输出结果:
1
1
第二种输出结果:
1
1
第三种输出结果:
2
1
为什么会这样呢? 上面说到过, 函数的作用域是静态的, 由函数声明的位置决定, 在哪里声明, 就决定了它的上层作用域是谁, 这与调用函数的位置无关. 无论在哪里调用, 它都会去函数本身的作用域中的命名空间找, 找不到在去上一层的命名空间找, 切记未必是在调用该函数的作用域的命名空间找. 对于第三种情况, 是最让我费解的地方, func = f1()执行完之后, f1 的命名空间被销毁, 按理说就找不到 i=2 了, 但是输出结果确实是 2, 所以我只能用 LEGB 搜索法则解释.(如果你知道为什么, 请给我留言, 感激不尽......)
(8)情况 8:
- class A(object):
- a = 2
- def fun(self):
- print(a)
- new_class = A()
- new_class.fun()
代码运行后报错: NameError: name 'a' is not defined. 上文中说过, Python 类成员变量与成员函数都有自己的作用域, 且各作用域平级.(用作用域的生命周期来解释也行, 但是真心觉得不对劲).
5 总结
Python 的作用域与命名空间有的时候真的让人很费解, 我本以为与 Java 等语言类似的, 没想多还是挺有区别的. 有些情况我到现在也没想通, 例如作用域与命名空间的生命周期, 用生命周期来解释上面的一些例子, 总觉得不对劲. 期间翻阅了 n 多前辈的博客资料, 到各有说法, 或许是我没理解到位, 若有前辈看到这里, 又刚好知道原因, 请为晚辈留言解惑, 感激不尽!
参考资料:
- https://www.jb51.net/article/114951.htm
- http://python.jobbole.com/86465/
- http://python.jobbole.com/81367/?utm_source=blog.jobbole.com&utm_medium=relatedPosts
来源: https://www.cnblogs.com/chenhuabin/p/10123009.html