>>> from env_helper import info; info()
页面更新时间: 2023-12-27 10:41:10
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

6.3. 理解名字查找机制

在Python中,所有所谓的变量,其实都是名字,这些名字指向一个或多个Python对象。

比如以下代码:

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

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

>>> locals()
{'__name__': '__main__',
 '__doc__': 'Automatically created module for IPython interactive environment',
 '__package__': None,
 '__loader__': None,
 '__spec__': None,
 '__builtin__': <module 'builtins' (built-in)>,
 '__builtins__': <module 'builtins' (built-in)>,
 '_ih': ['',
  "a=1nb=anc='china'nid(a)==id(b)",
  'id(a)==id(c)',
  'locals()',
  "a=1nb=anc='china'nid(a)==id(b)",
  'id(a)==id(c)',
  'locals()'],
 '_oh': {1: True, 2: False, 3: {...}, 4: True, 5: False},
 '_dh': ['/home/yubiao/jubook/_share/python_advanced/ch06_内部机制'],
 '_sh': <module 'IPython.core.shadowns' from '/usr/lib/python3/dist-packages/IPython/core/shadowns.py'>,
 'In': ['',
  "a=1nb=anc='china'nid(a)==id(b)",
  'id(a)==id(c)',
  'locals()',
  "a=1nb=anc='china'nid(a)==id(b)",
  'id(a)==id(c)',
  'locals()'],
 'Out': {1: True, 2: False, 3: {...}, 4: True, 5: False},
 'get_ipython': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x7fc46638ce10>>,
 'exit': <IPython.core.autocall.ZMQExitAutocall at 0x7fc4642b6cc0>,
 'quit': <IPython.core.autocall.ZMQExitAutocall at 0x7fc4642b6cc0>,
 '_': False,
 '__': True,
 '___': {...},
 '_i': 'id(a)==id(c)',
 '_ii': "a=1nb=anc='china'nid(a)==id(b)",
 '_iii': 'locals()',
 '_i1': "a=1nb=anc='china'nid(a)==id(b)",
 'a': 1,
 'b': 1,
 'c': 'china',
 '_1': True,
 '_i2': 'id(a)==id(c)',
 '_2': False,
 '_i3': 'locals()',
 '_3': {...},
 '_i4': "a=1nb=anc='china'nid(a)==id(b)",
 '_4': True,
 '_i5': 'id(a)==id(c)',
 '_5': False,
 '_i6': 'locals()'}

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

>>> globals()
{'__name__': '__main__',
 '__doc__': 'Automatically created module for IPython interactive environment',
 '__package__': None,
 '__loader__': None,
 '__spec__': None,
 '__builtin__': <module 'builtins' (built-in)>,
 '__builtins__': <module 'builtins' (built-in)>,
 '_ih': ['',
  "a=1nb=anc='china'nid(a)==id(b)",
  'id(a)==id(c)',
  'locals()',
  "a=1nb=anc='china'nid(a)==id(b)",
  'id(a)==id(c)',
  'locals()',
  'globals()'],
 '_oh': {1: True, 2: False, 3: {...}, 4: True, 5: False, 6: {...}},
 '_dh': ['/home/yubiao/jubook/_share/python_advanced/ch06_内部机制'],
 '_sh': <module 'IPython.core.shadowns' from '/usr/lib/python3/dist-packages/IPython/core/shadowns.py'>,
 'In': ['',
  "a=1nb=anc='china'nid(a)==id(b)",
  'id(a)==id(c)',
  'locals()',
  "a=1nb=anc='china'nid(a)==id(b)",
  'id(a)==id(c)',
  'locals()',
  'globals()'],
 'Out': {1: True, 2: False, 3: {...}, 4: True, 5: False, 6: {...}},
 'get_ipython': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x7fc46638ce10>>,
 'exit': <IPython.core.autocall.ZMQExitAutocall at 0x7fc4642b6cc0>,
 'quit': <IPython.core.autocall.ZMQExitAutocall at 0x7fc4642b6cc0>,
 '_': {...},
 '__': False,
 '___': True,
 '_i': 'locals()',
 '_ii': 'id(a)==id(c)',
 '_iii': "a=1nb=anc='china'nid(a)==id(b)",
 '_i1': "a=1nb=anc='china'nid(a)==id(b)",
 'a': 1,
 'b': 1,
 'c': 'china',
 '_1': True,
 '_i2': 'id(a)==id(c)',
 '_2': False,
 '_i3': 'locals()',
 '_3': {...},
 '_i4': "a=1nb=anc='china'nid(a)==id(b)",
 '_4': True,
 '_i5': 'id(a)==id(c)',
 '_5': False,
 '_i6': 'locals()',
 '_6': {...},
 '_i7': 'globals()'}

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

>>> def foo(x):
>>>     e = 1
>>>     f = e
>>>     g ='china'
>>>     print('*'*10)
>>>     print(globals())
>>>     print('*'*10)
>>>     print(c) # 打印全局变量c
>>> foo(1)
******
{'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', '__package__': None, '__loader__': None, '__spec__': None, '__builtin__': <module 'builtins' (built-in)>, '__builtins__': <module 'builtins' (built-in)>, '_ih': ['', "a=1nb=anc='china'nid(a)==id(b)", 'id(a)==id(c)', 'locals()', "a=1nb=anc='china'nid(a)==id(b)", 'id(a)==id(c)', 'locals()', 'globals()', "def foo(x):n    e = 1n    f = en    g ='china'n    print('*'10)n    print(globals)n    print(''10)n    print(c) # 打印全局变量cnfoo(1)", "def foo(x):n    e = 1n    f = en    g ='china'n    print(''10)n    print(globals())n    print(''10)n    print(c) # 打印全局变量cnfoo(1)"], '_oh': {1: True, 2: False, 3: {...}, 4: True, 5: False, 6: {...}, 7: {...}}, '_dh': ['/home/yubiao/jubook/_share/python_advanced/ch06_内部机制'], '_sh': <module 'IPython.core.shadowns' from '/usr/lib/python3/dist-packages/IPython/core/shadowns.py'>, 'In': ['', "a=1nb=anc='china'nid(a)==id(b)", 'id(a)==id(c)', 'locals()', "a=1nb=anc='china'nid(a)==id(b)", 'id(a)==id(c)', 'locals()', 'globals()', "def foo(x):n    e = 1n    f = en    g ='china'n    print(''10)n    print(globals)n    print(''10)n    print(c) # 打印全局变量cnfoo(1)", "def foo(x):n    e = 1n    f = en    g ='china'n    print(''10)n    print(globals())n    print(''10)n    print(c) # 打印全局变量cnfoo(1)"], 'Out': {1: True, 2: False, 3: {...}, 4: True, 5: False, 6: {...}, 7: {...}}, 'get_ipython': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x7fc46638ce10>>, 'exit': <IPython.core.autocall.ZMQExitAutocall object at 0x7fc4642b6cc0>, 'quit': <IPython.core.autocall.ZMQExitAutocall object at 0x7fc4642b6cc0>, '_': {...}, '__': {...}, '___': False, '_i': "def foo(x):n    e = 1n    f = en    g ='china'n    print(''10)n    print(globals)n    print(''10)n    print(c) # 打印全局变量cnfoo(1)", '_ii': 'globals()', '_iii': 'locals()', '_i1': "a=1nb=anc='china'nid(a)==id(b)", 'a': 1, 'b': 1, 'c': 'china', '_1': True, '_i2': 'id(a)==id(c)', '_2': False, '_i3': 'locals()', '_3': {...}, '_i4': "a=1nb=anc='china'nid(a)==id(b)", '_4': True, '_i5': 'id(a)==id(c)', '_5': False, '_i6': 'locals()', '_6': {...}, '_i7': 'globals()', '_7': {...}, '_i8': "def foo(x):n    e = 1n    f = en    g ='china'n    print(''10)n    print(globals)n    print(''10)n    print(c) # 打印全局变量cnfoo(1)", 'foo': <function foo at 0x7fc4641f2d90>, '_i9': "def foo(x):n    e = 1n    f = en    g ='china'n    print(''10)n    print(globals())n    print(''10)n    print(c) # 打印全局变量cnfoo(1)"}
*********
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进行申明也不能达到目的,其结果最终是在嵌套的函数所在 的命名空间中创建了一个新的变量。示例如下:

>>> def ex2():
>>>     var = 'a'
>>>     def inner():
>>>         global var
>>>         var = 'b'
>>>         print('inside inner,var is',var)
>>>     inner()
>>>     print('inside outer function,var is',var)
>>> ex2()
inside inner,var is b
inside outer function,var is a
  • 内置作用域: 这个相对简单,它是通过一个标准库中名__builtin__的模块来实 现的。

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

1)在最内层的莅围内杳找,一般而言,就是函数内部,local()里面査找。

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的实现中,只要出现了赋 值语句(或者称为名字绑定),那么这个名字就被当作局部变量来对待a所以在这里如果需要 改变全局变量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()
<function __main__.foo.<locals>.bar>

从上例中可以看出,在闭包bar()中,在编译代码为字节码时,因为存在a=b+l这条语 句,所以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)