返回介绍

建议56:理解名字查找机制

发布于 2024-01-30 22:19:09 字数 4433 浏览 0 评论 0 收藏 0

在Python中,所有所谓的变量,其实都是名字,这些名字指向一个或多个Python对象。比如以下代码:

>>> a = 1
>>> b = a
>>> c = 'china'
>>> id(a) == id(b)
True
>>> id(a) == id(c)
False

从中我们可以看出,名字a和b指向同一个Python对象,即一个int类型的对象,这个对象的值为1;而c则指向另一个Python对象,它是一个str类型的对象。所有的这些名字,都存在于一个表里(又称为命名空间),一般情况下,我们称之为局部变量(locals),可以通过locals()函数调用看到。

>>> locals()
{'a': 1, 'c': 'china', 'b': 1, '__builtins__': <module '__builtin__' (built-in)>, 
     '__package__': None, '__name__': '__main__', '__doc__': None}

现在我们是直接在Python shell中执行这一些代码,实际上这些变量也是全局的,所以在一个叫globals的表里也可以看到。

>>> globals()
{'a': 1, 'c': 'china', 'b': 1, '__builtins__': <module '__builtin__' (built-in)>, 
     '__package__': None, '__name__': '__main__', '__doc__': None}

如果我们在一个函数里面定义这些变量,情况会有所不同。

>>> def foo(x):
...   e = 1
...   f = e
...   g = 'china'
...   print locals()
...   print '#' * 10
...   print globals()
...   print '#' * 10
...   print c            
①打印全局变量c
... 
>>> foo(1)
{'e': 1, 'g': 'china', 'f': 1, 'x': 1}
##########
{'a': 1, 'c': 'china', 'b': 1, '__builtins__': <module '__builtin__' (built-
  in)>, '__package__': None, '__name__': '__main__', 'foo': <function foo at 
  0x104fad320>, '__doc__': None}
##########
china

可以看到函数中的locals()返回值并不包含之前定义在全局中的a、b、c等名字,只有定义在函数内的e、f、g和函数形参x,这是什么原因呢?要回答这个问题,首先要理解Python中变量的作用域。

Python中所有的变量名都是在赋值的时候生成的,而对任何变量名的创建、查找或者改变都会在命名空间(namespace)中进行。变量名所在的命名空间直接决定了其能访问到的范围,即变量的作用域。Python中的作用域自Python2.2之后分为局部作用域(local)、全局作用域(Global)、嵌套作用域(enclosing functions locals)以及内置作用域(Build-in)这4种。

局部作用域:一般来说函数的每次调用都会创建一个新的本地作用域,拥有新的命名空间。因此函数内的变量名可以与函数外的其他变量名相同,由于其命名空间不同,并不会产生冲突。默认情况下函数内部任意的赋值操作(包括=语句、import语句、def语句、参数传递等)所定义的变量名,如果没用global语句,则申明都为局部变量,即仅在该函数内可见。

全局作用域:定义在Python模块文件中的变量名拥有全局作用域,需要注意的是这里的全局仅限单个文件,即在一个文件的顶层的变量名仅在这个文件内可见,并非所有的文件,其他文件中想使用这些变量名必须先导入文件对应的模块。当在函数之外给一个变量名赋值时是在其全局作用域的情况下进行的。

嵌套作用域:一般在多重函数嵌套的情况下才会考虑到。需要注意的是global语句仅针对全局变量,在嵌套作用域的情况下,如果想在嵌套的函数内修改外层函数中定义的变量,即使使用global进行申明也不能达到目的,其结果最终是在嵌套的函数所在的命名空间中创建了一个新的变量。示例如下:

  var = 'a'
  def inner():
    global var
    var = 'b'
    print 'inside inner, var is ', var
  inner()
  print 'inside outer function, var is ', var

上述程序的输出如下:

inside inner, var is  b
inside outer function, var is  a

内置作用域:这个相对简单,它是通过一个标准库中名为__builtin__的模块来实现的。

回到前面代码中标注①的语句print c,仍然正确输出了china这个值。这是因为当访问一个变量的时候,其查找顺序遵循变量解析机制LEGB法则,即依次搜索4个作用域:局部作用域、嵌套作用域、全局作用域以及内置作用域,并在第一个找到的地方停止搜寻,如果没有搜到,则会抛出异常。因此当存在多个同名变量的时候,操作生效的往往是搜索顺序在前的。具体来说Python的名字查找机制如下:

1)在最内层的范围内查找,一般而言,就是函数内部,即在locals()里面查找。

2)在模块内查找,即在globals()里面查找。

3)在外层查找,即在内置模块中查找,也就是在__builtin__中查找。

至此,我们可以理解清楚能够在foo()函数中访问到名字c的原因在于当Python在局部变量中找不到c时,它会尝试在模块级的全局变量中查找,并成功地找到该名字。

不过,当我们试图改变全局变量的值时,事情可能跟想象的稍有不同。

>>> def bar():
...  c = 'america'
...  print c
... 
>>> bar()
america
>>> print c
china

真奇怪!不是吗?在bar()函数中修改c的值,并没有修改到全局变量的c,而是好像bar()函数有了一个局部变量c一样!事实上确实如此,在CPython的实现中,只要出现了赋值语句(或者称为名字绑定),那么这个名字就被当作局部变量来对待。所以在这里如果需要改变全局变量c的值,就需要使用global关键字。

>>> def bar():
...   global c
...   c = 'america'
...   print c
... 
>>> bar()
america
>>> print c
america

不过,随着更多Python特性的加入,事情变得更加复杂起来。比如在Python闭包中,有这样的问题:

>>> def foo():
...  a = 1
...  def bar():
...       b = a * 2
...       a = b + 1
...       print a
...  return bar
... 
>>> foo()()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 4, in bar
UnboundLocalError: local variable 'a' referenced before assignment

从上例中可以看出,在闭包bar()中,在编译代码为字节码时,因为存在a=b+1这条语句,所以a被当作了局部变量看待,而执行时因为b=a*2先执行,此时局部变量a尚不存在,所以产生了一个UnboundLocalError。在Python2.x中可以使用global关键字解决部分问题,先把a创建为一个模块全局变量,然后在所有读写(包括只是访问)该变量的作用域中都要先使用global声明其为全局变量。

>>> a = 1
>>> def foo(x):
...  global a
...  a = a * x
...  def bar():
...       global a
...       b = a * 2
...       a = b + 1
...       print a
...  return bar
... 
>>> foo(1)()
3

这种方案抛开编程语言并不提倡全局变量不谈,有的时候还影响业务逻辑。此外,还有把a作为容器的一个元素来对待的方案,但也都相当复杂。真正的解决方案是Python3引入的nonlocal关键字,通过它能够解决这方面的问题。

>>> def foo(x):
...  a = x
...  def bar():
...      nonlocal a
...      b = a * 2
...      a = b + 1
...      print(a)
...  return bar
... 
>>> bar1  = foo(1)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文