1 引言

命名空间与作用域是程序设计中的基础概念,深入理解有助于理解变量的生命周期,减少代码中的莫名其妙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程序(文本)的某一段或某几段,在这些地方,某个命名空间中的名字可以被直接引用。这部分程序就是这个命名空间的作用域。只有函数、类、模块会产生新的作用域,代码块(例如iffor代码块)不会产生新的作用域。

  另外,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

Python中命名空间与作用域使用总结的更多相关文章

  1. Python进阶 - 命名空间与作用域

    Python进阶 - 命名空间与作用域 写在前面 如非特别说明,下文均基于Python3 命名空间与作用于跟名字的绑定相关性很大,可以结合另一篇介绍Python名字.对象及其绑定的文章. 1. 命名空 ...

  2. Python中变量的作用域(variable scope)

    http://www.crifan.com/summary_python_variable_effective_scope/ 解释python中变量的作用域 示例: 1.代码版 #!/usr/bin/ ...

  3. python之命名空间与作用域

    一.命名空间与作用域 在命名空间中的名称能将任何python对象作为值,在不同的命名空间中相同的名称可以与不同的对象相关联.但是,如果存在名称解析协议,则多个命名空间可以一起工作来解析名称.也就是说, ...

  4. Python中的变量作用域

    python中变量作用域包括: L (Local) 局部作用域,函数内部声明但没有使用global的变量E (Enclosing) 闭包函数外的函数中,def或者lambda的本地作用域G (Glob ...

  5. python学习之【第九篇】:Python中的变量作用域

    1.前言 Python 中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的. 2.变量作用域 变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称.Python的作 ...

  6. Python的命名空间及作用域

    命名空间的分类 全局命名空间 是在程序从上到下被执行的过程中依次加载进内存的:放置了我们设置的所有变量名和函数名 局部命令空间 就是函数内部定义的名字:当调用函数的时候 才会产生这个名称空间 随着函数 ...

  7. Python中变量的作用域

    一.变量作用域的含义 变量的作用域说白了就是变量的值从哪里获取,或者说变量取值的地方 我们在写代码过程中会用到很多变量,这些变量会出现在各种代码块中,有的出现在函数块里,有的在函数块外,例如: def ...

  8. python中global的作用域

    #python引用变量的顺序: 当前作用域局部变量->外层作用域变量->当前模块中的全局变量->python内置变量 . ''' a=30 声明为全局变量 a=20 为test()函 ...

  9. python中nonlocal 的作用域

    ''' nonlocal关键字用来在函数或其他作用域中使用外层(非全局)变量. ''' def work(): x = 0 def new_work(): nonlocal x x=x+3 retur ...

随机推荐

  1. eos 源码net_plugin分析

    1 net_plugin_impl::connect(connection_ptr c) 函数用于解析地址,内部异步回调async_resolve async_resolve 传递了lambda表达式 ...

  2. 手动部署一个单节点kubernetes

    目录 简要说明 安装环境说明 部署 生成相关证书 证书类型说明 安装cfssl证书生成工具 生成CA证书 生成Kubernetes master节点使用的证书 生成kubectl证书 生成kube-p ...

  3. 2018年11月25日ICPC焦作站参赛总结

    可能就这么退役了吧. 对这次ICPC还是比较有信心的,毕竟心态都放平和了. 路途很波折,热身赛还是赶上了. 等到了正赛的时候,开场看出了A题的签到,签到肯定是我来签的,11分钟签完了这道题之后,开始看 ...

  4. 怎样提高WebService的性能

    服务器端WebService程序: using System.Runtime.Serialization.Formatters.Binary; using System.IO; using Syste ...

  5. BZOJ2428 均分数据

    2428: [HAOI2006]均分数据 Time Limit: 5 Sec  Memory Limit: 128 MB Description 已知N个正整数:A1.A2.…….An .今要将它们分 ...

  6. 分布式文件系统 之 数据块(Block)

    众所周知,HDFS中以数据块(block)为单位进行存储管理.本文简单介绍一下HDFS中数据块(block)的概念,以及众多分布式存储系统(不止是HDFS)使用block作为存储管理基本单位的意义. ...

  7. iOS必学技-cocoapods

    我就不再造轮子了,网上的教程很详细,楼主亲测,好用. http://code4app.com/article/cocoapods-install-usage 楼主安装使用过程中遇到以下几个问题,同学们 ...

  8. ajax函数说明

    url: 要求为String类型的参数,(默认为当前页地址)发送请求的地址. type: 要求为String类型的参数,请求方式(post或get)默认为get.注意其他http请求方法,例如put和 ...

  9. HTML5 CSS Reset

    最近在学习HTML和CSS,发现一个不错的模板,放于此处. /* html5doctor.com Reset Stylesheet v1.6.1 Last Updated: 2010-09-17 Au ...

  10. 洛谷4951 地震 bzoj1816扑克牌 洛谷3199最小圈 / 01分数规划

    洛谷4951 地震 #include<iostream> #include<cstdio> #include<algorithm> #define go(i,a,b ...