返回介绍

4.2 字典和命名空间

发布于 2024-01-25 21:44:08 字数 2959 浏览 0 评论 0 收藏 0

字典的查询很快,不过,在不必要的时候这么做会让你的代码变慢,就跟任何非必要代码行一样。Python在命名空间的管理上就浮现出这一问题,它过度使用了字典来进行查询。

每当Python访问一个变量、函数或模块时,都有一个体系来决定它去哪里查找这些对象。首先,Python查找locals()数组,其内保存了所有本地变量的条目。Python花了很多精力优化本地变量查询的速度,而这也是整条链上唯一一个不需要字典查询的部分。如果它不在本地变量里,那么会搜索globals()字典。最后,如果对象也不在那里,则搜索__builtin__对象。要注意locals()和globals()是显式的字典而__builtin__则是模块对象,在搜索__builtin__中的一个属性时,我们其实是在搜索它的locals()字典(对所有的模块对象和类对象都是如此!)。

为了让事情更清楚,让我们看一个简单的例子,对不同作用域内的函数进行调用(例4-8)。我们可以用dis模块(例4-9)解析函数来更好地理解这些命名空间查询是怎么发生的。

例4-8 命名空间查询

import math
from math import sin

def test1(x):
  """
  >>> %timeit test1(123456)
  1000000 loops, best of 3: 381 ns per loop
  """
  return math.sin(x)

def test2(x):
  """
  >>> %timeit test2(123456)
  1000000 loops, best of 3: 311 ns per loop
  """
  return sin(x)

def test3(x, sin=math.sin):
  """
  >>> %timeit test3(123456)
  1000000 loops, best of 3: 306 ns per loop
  """
  return sin(x)

例4-9 命名空间查询解析

>>> dis.dis(test1)
  9       0 LOAD_GLOBAL      0 (math) # Dictionary lookup
        3 LOAD_ATTR      1 (sin)  # Dictionary lookup
        6 LOAD_FAST      0 (x)  # Local lookup
        9 CALL_FUNCTION    1
       12 RETURN_VALUE

>>> dis.dis(test2)
  15      0 LOAD_GLOBAL      0 (sin) # Dictionary lookup
        3 LOAD_FAST      0 (x)   # Local lookup
        6 CALL_FUNCTION    1
        9 RETURN_VALUE

>>> dis.dis(test3)
  21      0 LOAD_FAST      1 (sin) # Local lookup
        3 LOAD_FAST      0 (x)   # Local lookup
        6 CALL_FUNCTION    1
        9 RETURN_VALUE

第一个函数test1显示查询math库来调用sin。生成的字节码的证据表明:首先一个math模块的引用必须被调入,然后在模块上进行属性查询,直到找到sin函数。整个步骤经过了两次字典查询,一次查找math模块,一次在模块中查找sin函数。

另一方面,test2从math模块显式导入了sin函数,因此该函数可在全局命名空间中被直接访问。这意味着我们可以避免查询math模块以及后续的属性查询。不过,我们还是要在全局命名空间查找sin函数。这也是另一个我们要从模块中显式导入函数的原因。这样做不仅让代码更可读,因为读者可以知道到底需要外部资源中的什么函数,而且也加速了代码!

最后,test3定义了sin函数为一个参数关键字,其默认值是math模块的sin函数的引用。虽然我们依然需要在模块中查找这一函数,但仅在test3函数第一次被定义时查找。之后,这一引用以默认参数关键字的形式作为一个本地变量被保存在函数的定义中。之前提到过,本地变量无须字典查询;它们被保存在一个十分微小的数组中,具有很快的查询速度。因此,找到这个函数非常快。

这些效果只是Python对命名空间管理方式的一个有趣结果,test3并不是Python的惯用写法。幸运的是,这些额外的字典查询仅在它们被大量调用时才会开始降低性能(比如朱利亚集合例子中在一个高速循环的最内部)。记住这一点,一个更可读的解决方案是在循环开始前设置一个本地变量保存一个函数的全局引用。在调用函数时我们依然会进行一次全局查询,但在循环内对函数的每次调用都会变快。考虑到代码中即使是十分微小的减慢也会被数百万次的运行所放大,即使一次字典查询仅需花费几百纳秒,如果我们循环几百万次这样的查询,那总耗时就会迅速累加。事实上,例4-10的查询中,我们可以看到只需在循环前将sin函数本地化就能获得9.4%的速度提升。

例4-10 循环内的命名空间查询的降速效果

from math import sin

def tight_loop_slow(iterations):
  """
  >>> %timeit tight_loop_slow(10000000)
  1 loops, best of 3: 2.21 s per loop
  """
  result = 0
  for i in xrange(iterations):
    # this call to sin requires a global lookup
    result += sin(i)

def tight_loop_fast(iterations):
  """
  >>> %timeit tight_loop_fast(10000000)
  1 loops, best of 3: 2.02 s per loop
  """
  result = 0
  local_sin = sin
  for i in xrange(iterations):
    # this call to local_sin requires a local lookup
    result += local_sin(i)

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

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

发布评论

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