- 内容提要
- 前言
- 作者简介
- 封面简介
- 第1章 理解高性能 Python
- 第2章 通过性能分析找到瓶颈
- 2.1 高效地分析性能
- 2.2 Julia 集合的介绍
- 2.3 计算完整的 Julia 集合
- 2.4 计时的简单方法——打印和修饰
- 2.5 用 UNIX 的 time 命令进行简单的计时
- 2.6 使用 cProfile 模块
- 2.7 用 runsnakerun 对 cProfile 的输出进行可视化
- 2.8 用 line_profiler 进行逐行分析
- 2.9 用 memory_profiler 诊断内存的用量
- 2.10 用 heapy 调查堆上的对象
- 2.11 用 dowser 实时画出变量的实例
- 2.12 用 dis 模块检查 CPython 字节码
- 2.13 在优化期间进行单元测试保持代码的正确性
- 2.14 确保性能分析成功的策略
- 2.15 小结
- 第3章 列表和元组
- 第4章 字典和集合
- 第5章 迭代器和生成器
- 第6章 矩阵和矢量计算
- 第7章 编译成 C
- 第8章 并发
- 第9章 multiprocessing 模块
- 第10章 集群和工作队列
- 第11章 使用更少的 RAM
- 第12章 现场教训
4.2 字典和命名空间
字典的查询很快,不过,在不必要的时候这么做会让你的代码变慢,就跟任何非必要代码行一样。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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论