8.5. localsglobals

让我们暂时放下 HTML 处理,谈谈 Python 如何处理变量。 Python 有两个内置函数,localsglobals,它们提供基于字典的访问局部变量和全局变量的方式。

还记得 locals 吗?你第一次见到它是在这里

    def unknown_starttag(self, tag, attrs):
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

不,等等,你还不能学习 locals。首先,你需要了解命名空间。这部分内容可能比较枯燥,但很重要,所以请集中注意力。

Python 使用称为命名空间的东西来跟踪变量。命名空间就像一个字典,其中键是变量的名称,字典值是这些变量的值。事实上,你可以像访问 Python 字典一样访问命名空间,你马上就会看到。

Python 程序中的任何特定点,都有几个可用的命名空间。每个函数都有自己的命名空间,称为局部命名空间,它跟踪函数的变量,包括函数参数和局部定义的变量。每个模块都有自己的命名空间,称为全局命名空间,它跟踪模块的变量,包括函数、类、任何其他导入的模块,以及模块级变量和常量。还有一个内置命名空间,可以从任何模块访问,它包含内置函数和异常。

当一行代码请求变量 x 的值时,Python 将按顺序在所有可用的命名空间中搜索该变量

  1. 局部命名空间 - 特定于当前函数或类方法。如果函数定义了一个局部变量 x,或者有一个参数 xPython 将使用它并停止搜索。
  2. 全局命名空间 - 特定于当前模块。如果模块定义了一个名为 x 的变量、函数或类,Python 将使用它并停止搜索。
  3. 内置命名空间 - 对所有模块全局有效。作为最后的手段,Python 将假设 x 是内置函数或变量的名称。

如果 Python 在任何这些命名空间中都找不到 x,它就会放弃并引发一个 NameError,并显示消息 没有名为 'x' 的变量,你在 示例 3.18,“引用未绑定变量” 中已经看到过,但你当时可能没有意识到 Python 在给你这个错误之前做了多少工作。

Important
Python 2.2 引入了一个微妙但重要的变化,它影响了命名空间搜索顺序:嵌套作用域。在 2.2 之前的 Python 版本中,当你引用 嵌套函数lambda 函数 中的变量时,Python 将在当前(嵌套或 lambda)函数的命名空间中搜索该变量,然后在模块的命名空间中搜索。 Python 2.2 将在当前(嵌套或 lambda)函数的命名空间中搜索该变量,然后在父函数的命名空间中搜索,然后在模块的命名空间中搜索。 Python 2.1 可以采用这两种方式中的任何一种;默认情况下,它的工作方式与 Python 2.0 相同,但你可以在模块的顶部添加以下代码行,使你的模块像 Python 2.2 一样工作

from __future__ import nested_scopes

你是否感到困惑?不要绝望!我保证这真的很酷。就像 Python 中的许多东西一样,命名空间在 运行时可以直接访问。怎么做?嗯,可以通过内置的 locals 函数访问局部命名空间,并可以通过内置的 globals 函数访问全局(模块级)命名空间。

示例 8.10. 介绍 locals

>>> def foo(arg): 1
...     x = 1
...     print locals()
...     
>>> foo(7)        2
{'arg': 7, 'x': 1}
>>> foo('bar')    3
{'arg': 'bar', 'x': 1}
1 函数 foo 在其局部命名空间中有两个变量:arg,其值传递给函数,以及 x,它是在函数内部定义的。
2 locals 返回一个名称/值对的字典。此字典的键是作为字符串的变量名称;字典的值是变量的实际值。因此,使用 7 调用 foo 将打印包含函数的两个局部变量的字典:arg (7) 和 x (1)。
3 请记住,Python 具有动态类型,因此你可以很容易地为 arg 传递一个字符串;函数(以及对 locals 的调用)仍然可以正常工作。 locals 适用于所有数据类型的变量。

locals 对局部(函数)命名空间的作用,globals 对全局(模块)命名空间的作用相同。 不过,globals 更令人兴奋,因为模块的命名空间更令人兴奋。[3] 模块的命名空间不仅包括模块级变量和常量,还包括模块中定义的所有函数和类。 此外,它还包括导入到模块中的任何内容。

还记得 from module importimport module 之间的区别吗? 使用 import module,模块本身被导入,但它保留了自己的命名空间,这就是为什么你需要使用模块名称来访问它的任何函数或属性:module.function。 但是使用 from module import,你实际上是将特定函数和属性从另一个模块导入到你自己的命名空间中,这就是为什么你可以直接访问它们而无需引用它们来自的原始模块。 使用 globals 函数,你可以实际看到这种情况的发生。

示例 8.11. 介绍 globals

查看 BaseHTMLProcessor.py 底部的以下代码块


if __name__ == "__main__":
    for k, v in globals().items():             1
        print k, "=", v
1 为了避免你感到害怕,请记住你以前见过所有这些。 globals 函数返回一个字典,你正在使用 items 方法和 多变量赋值 迭代字典。 这里唯一的新内容是 globals 函数。

现在从命令行运行脚本会给出以下输出(请注意,你的输出可能略有不同,取决于你的平台和 Python 的安装位置)

c:\docbook\dip\py> python BaseHTMLProcessor.py
SGMLParser = sgmllib.SGMLParser                1
htmlentitydefs = <module 'htmlentitydefs' from 'C:\Python23\lib\htmlentitydefs.py'> 2
BaseHTMLProcessor = __main__.BaseHTMLProcessor 3
__name__ = __main__                            4
... rest of output omitted for brevity...
1 SGMLParser 是使用 from module importsgmllib 导入的。 这意味着它是直接导入到模块的命名空间中的,它就在这里。
2 将此与使用 import 导入的 htmlentitydefs 进行对比。 这意味着 htmlentitydefs 模块本身在命名空间中,但在 htmlentitydefs 中定义的 entitydefs 变量不在命名空间中。
3 此模块只定义了一个类,BaseHTMLProcessor,它就在这里。 请注意,这里的值是 类本身,而不是类的特定实例。
4 还记得 if __name__ 技巧 吗? 当运行模块(而不是从另一个模块导入它)时,内置的 __name__ 属性是一个特殊值,__main__。 由于你从命令行将此模块作为脚本运行,因此 __name____main__,这就是为什么执行用于打印 globals 的小测试代码的原因。
Note
使用 localsglobals 函数,你可以动态获取任意变量的值,方法是将变量名称作为字符串提供。 这反映了 getattr 函数的功能,该函数允许你通过将函数名称作为字符串提供来动态访问任意函数。

localsglobals 函数之间还有一个重要区别,你应该现在就学习它,以免以后被它困扰。 它迟早会困扰你,但至少到那时你还会记得学过它。

示例 8.12. locals 是只读的,globals 不是


def foo(arg):
    x = 1
    print locals()    1
    locals()["x"] = 2 2
    print "x=",x      3

z = 7
print "z=",z
foo(3)
globals()["z"] = 8    4
print "z=",z          5
1 由于 foo 是使用 3 调用的,因此这将打印 {'arg': 3, 'x': 1}。 这应该不足为奇。
2 locals 是一个返回字典的函数,这里你正在该字典中设置一个值。 你可能认为这会将局部变量 x 的值更改为 2,但事实并非如此。 locals 实际上并不返回局部命名空间,它返回一个副本。 因此,更改它不会对局部命名空间中变量的值产生任何影响。
3 这将打印 x= 1,而不是 x= 2
4 在被 locals 灼伤之后,你可能会认为这不会 更改 z 的值,但事实并非如此。 由于 Python 实现方式的内部差异(我不想对此进行深入探讨,因为我自己也没有完全理解它们),globals 返回的是实际的全局命名空间,而不是副本:这与 locals 的行为完全相反。 因此,对 globals 返回的字典的任何更改都会直接影响你的全局变量。
5 这将打印 z= 8,而不是 z= 7

脚注

[3] 我不太出门。