【问题标题】:Comparison: import statement vs __import__ function比较:import 语句与 __import__ 函数
【发布时间】:2017-09-12 12:03:10
【问题描述】:

作为Using builtin __import__() in normal cases 问题的后续,我进行了一些测试,结果令人惊讶。

我在这里比较经典import 语句的执行时间,以及对__import__ 内置函数的调用。 为此,我在交互模式下使用以下脚本:

import timeit   

def test(module):    
    t1 = timeit.timeit("import {}".format(module))
    t2 = timeit.timeit("{0} = __import__('{0}')".format(module))
    print("import statement:   ", t1)
    print("__import__ function:", t2)
    print("t(statement) {} t(function)".format("<" if t1 < t2 else ">"))

在链接的问题中,这里是导入 sys 时的比较,以及其他一些标准模块:

>>> test('sys')
import statement:    0.319865173171288
__import__ function: 0.38428380458522987
t(statement) < t(function)

>>> test('math')
import statement:    0.10262547545597034
__import__ function: 0.16307580163101054
t(statement) < t(function)

>>> test('os')
import statement:    0.10251490255312312
__import__ function: 0.16240755669640627
t(statement) < t(function)

>>> test('threading')
import statement:    0.11349136644972191
__import__ function: 0.1673617034957573
t(statement) < t(function)

到目前为止一切顺利,import__import__() 快。 这对我来说很有意义,因为正如我在链接的帖子中所写,我发现 IMPORT_NAME 指令与 CALL_FUNCTION 相比进行了优化是合乎逻辑的,因为后者导致调用 __import__

但是当涉及到较少标准的模块时,结果却相反:

>>> test('numpy')
import statement:    0.18907936340054476
__import__ function: 0.15840019037769792
t(statement) > t(function)

>>> test('tkinter')
import statement:    0.3798560809537861
__import__ function: 0.15899962771786136
t(statement) > t(function)

>>> test("pygame")
import statement:    0.6624641952621317
__import__ function: 0.16268579177259568
t(statement) > t(function)

执行时间差异背后的原因是什么? import 语句在标准模块上更快的实际原因是什么? 另一方面,为什么__import__ 函数与其他模块一起使用更快?

测试领先于 Python 3.6

【问题讨论】:

  • 我假设内置模块可能被提前缓存在某处。
  • @cᴏʟᴅsᴘᴇᴇᴅ 我不想放弃线索并影响潜在的回答者,但是......是的,我认为这是相关的。

标签: python performance python-import


【解决方案1】:

timeit 测量总执行时间,但模块的第一次导入,无论是通过import 还是__import__,都比后续导入慢 - 因为它是唯一实际执行模块初始化的模块。它必须在文件系统中搜索模块的文件,加载模块的源代码(最慢)或先前创建的字节码(慢但比解析.py 文件快一点)或共享库(对于 C 扩展),执行初始化代码,并将模块对象存储在sys.modules中。随后的导入将跳过所有这些并从 sys.modules 检索模块对象。

如果你颠倒顺序,结果会有所不同:

import timeit   

def test(module):    
    t2 = timeit.timeit("{0} = __import__('{0}')".format(module))
    t1 = timeit.timeit("import {}".format(module))
    print("import statement:   ", t1)
    print("__import__ function:", t2)
    print("t(statement) {} t(function)".format("<" if t1 < t2 else ">"))

test('numpy')
import statement:    0.4611093703134608
__import__ function: 1.275512785926014
t(statement) < t(function)

获得无偏见结果的最佳方法是导入一次,然后进行计时:

import timeit   

def test(module):    
    exec("import {}".format(module))
    t2 = timeit.timeit("{0} = __import__('{0}')".format(module))
    t1 = timeit.timeit("import {}".format(module))
    print("import statement:   ", t1)
    print("__import__ function:", t2)
    print("t(statement) {} t(function)".format("<" if t1 < t2 else ">"))

test('numpy')
import statement:    0.4826306561727307
__import__ function: 0.9192819125911029
t(statement) < t(function)

所以,是的,import 总是比 __import__ 快。

【讨论】:

  • 我认为timeit 在执行后清理了上下文......看来我错了。那么,import__import__ 快的原因其实和IMPORT_NAME 字节码指令有关?
  • 不,它没有。它只是多次评估设置,哪种代表清理。但是对于导入:这些在运行之间被缓存(参见例如this gist
  • @Rightleg 一旦一个模块被加载(本地或全局)它总是停留在sys.modules,未来的导入只是从那里获取它(除非你从sys.modules删除它),因此不会发生清理.名称查找和函数调用是 __import__ 速度慢的原因。
  • @Rightleg 我不确定速度差异的原因,但是是的,字节码比查找名称 __import__ 并调用函数要快。即使该函数与字节码的作用相同。但是字节码的实际实现和函数调用之间也可能存在很多差异。我的意思是function 带有很多语句不需要的参数。
  • import 还会查找 __import__ 名称,因为它必须检查 __import__ 是否被替换。不过,如果 __import__ 没有被替换,import 可以走一条捷径。
【解决方案2】:

请记住,所有模块在第一次导入后都会缓存到sys.modules,所以时间...

不管怎样,我的结果是这样的:

#!/bin/bash

itest() {
    echo -n "import $1: "
    python3 -m timeit "import $1"
    echo -n "__import__('$1'): "
    python3 -m timeit "__import__('$1')"
}

itest "sys"
itest "math"
itest "six"
itest "PIL"
  • import sys: 0.481
  • __import__('sys'): 0.586
  • import math: 0.163
  • __import__('math'): 0.247
  • import six: 0.157
  • __import__('six'): 0.273
  • import PIL:0.162
  • __import__('PIL'): 0.265

【讨论】:

    【解决方案3】:

    执行时间差异背后的原因是什么?

    import 语句有一条非常简单的路径。它导致IMPORT_NAME 调用import_name 并导入给定的模块(如果没有覆盖名称__import__):

    dis('import math')
      1           0 LOAD_CONST               0 (0)
                  2 LOAD_CONST               1 (None)
                  4 IMPORT_NAME              0 (math)
                  6 STORE_NAME               0 (math)
                  8 LOAD_CONST               1 (None)
                 10 RETURN_VALUE
    

    另一方面,__import__ 执行所有函数通过 CALL_FUNCTION 执行的通用函数调用步骤:

    dis('__import__(math)')
      1           0 LOAD_NAME                0 (__import__)
                  2 LOAD_NAME                1 (math)
                  4 CALL_FUNCTION            1
                  6 RETURN_VALUE
    

    当然,它是内置的,比普通的 py 函数快得多,但它仍然比带有 import_nameimport 语句慢。

    这就是为什么它们之间的时间差是恒定的。使用@MSeifert sn-p(纠正了不公正的时间:-)并添加另一个打印,您可以看到:

    import timeit   
    
    def test(module):    
        exec("import {}".format(module))
        t2 = timeit.timeit("{0} = __import__('{0}')".format(module))
        t1 = timeit.timeit("import {}".format(module))
        print("import statement:   ", t1)
        print("__import__ function:", t2)
        print("t(statement) {} t(function)".format("<" if t1 < t2 else ">"))
        print('Diff: {}'.format(t2-t1))
    
    
    for m in sys.builtin_module_names:
        test(m)
    

    在我的机器上,它们之间存在大约 0.17 的恒定差异(通常会出现轻微的差异)

    *值得注意的是,这些并不是完全等效的。正如字节码所证明的那样,__import__ 没有进行任何名称绑定。

    【讨论】:

      猜你喜欢
      • 2017-11-23
      • 1970-01-01
      • 1970-01-01
      • 2019-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多