Python的命名空间、作用域的使用与Java、C++等语言有很大差异,若不注意,容易采坑。本文对Python中命名空间与作用域进行详细解析,并通过案例分析了Python命名空间、作用域在使用中容易出错的情况。

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

版权声明:本文为chenhuabin原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/chenhuabin/p/10123009.html