比较:import语句 vs __import__函数

11
作为问题“在普通情况下使用内置的__import__()”的后续,我进行了一些测试,并得出了令人惊讶的结果。
我在此比较了经典的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__时,我发现IMPORT_NAME指令与CALL_FUNCTION相比进行了优化。
但是,对于非标准模块,结果则相反:
>>> 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版本。

我假设内置模块在某处预先被缓存的可能性。 - cs95
@cᴏʟᴅsᴘᴇᴇᴅ 我不想放弃线索并影响潜在的回答者,但是... 是的,我认为那是相关的。 - Right leg
3个回答

11

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字节码指令有关吗? - Right leg
不,它并没有。它只是多次评估设置,这有点类似于清理。但是对于导入:它们在运行之间被缓存(例如,请参见此代码片段)。 - MSeifert
1
@Rightleg 一旦加载了一个模块(无论是本地还是全局),它就始终存在于sys.modules中,因此将来的导入只需从其中获取它(除非你从sys.modules中删除它),因此不需要进行清理。名称查找和函数调用是__import__缓慢的原因。 - Ashwini Chaudhary
1
@Rightleg 我不确定速度差异的原因,但是字节码确实比查找名称 __import__ 和调用函数要快。即使函数只是执行与字节码相同的操作。但是,在字节码和函数调用的实际实现中可能存在很多差异。我的意思是,函数需要很多语句不需要的参数。 - MSeifert
1
import 还会查找 __import__ 名称,因为它必须检查是否已替换 __import__。如果 __import__ 没有被替换,import 就可以走快速路径。 - user2357112

4
记住,所有模块在第一次导入后都会被缓存到 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

这是一张图片,请点击链接查看


4
这种执行时间差异背后的原因是什么?
导入语句有一个相当简单的路径。它会引导到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

当然,它是内置的,所以比普通的Python函数更快,但仍然比使用import_name语句的import语句慢。

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

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__ 不执行任何名称绑定。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接