6

我正在尝试使用 Python 中的 timeit 模块(编辑:我们正在使用 Python 3)来决定几个不同的代码流。在我们的代码中,我们有一系列 if 语句来测试字符串中是否存在字符代码,如果存在则将其替换为:

if "<substring>" in str_var:
    str_var = str_var.replace("<substring>", "<new_substring>")

我们为不同的子字符串多次这样做。我们正在讨论它和只使用这样的替换:

str_var = str_var.replace("<substring>", "<new_substring>")

我们尝试使用 timeit 来确定哪个更快。如果上面的第一个代码块是“stmt1”,第二个是“stmt2”,我们的设置字符串看起来像

str_var = '<string><substring><more_string>',

我们的 timeit 语句将如下所示:

timeit.timeit(stmt=stmt1, setup=setup)

timeit.timeit(stmt=stmt2, setup=setup)

现在,像这样运行它,在我们的两台笔记本电脑上(相同的硬件,相似的处理负载)stmt1(带有 if 语句的语句)即使在多次运行后也运行得更快(百分之三到四秒对四分之一秒) stmt2 的秒数)。

但是,如果我们像这样定义函数来做这两件事(包括创建变量的设置):

def foo():
    str_var = '<string><substring><more_string>'
    if "<substring>" in str_var:
        str_var = str_var.replace("<substring>", "<new_substring>")

def foo2():
    str_var = '<string><substring><more_string>'
    str_var = str_var.replace("<substring>", "<new_substring>")

并像运行 timeit 一样:

timeit.timeit("foo()", setup="from __main__ import foo")
timeit.timeit("foo2()", setup="from __main__ import foo2")

没有 if 语句 (foo2) 的语句运行得更快,与无功能的结果相矛盾。

我们是否遗漏了有关 Timeit 工作原理的一些信息?或者 Python 如何处理这样的案例?

编辑这里是我们的实际代码:

>>> def foo():
    s = "hi 1 2 3"
    s = s.replace('1','5')

>>> def foo2():
    s = "hi 1 2 3"
    if '1' in s:
        s = s.replace('1','5')


>>> timeit.timeit(foo, "from __main__ import foo")
0.4094226634183542
>>> timeit.timeit(foo2, "from __main__ import foo2")
0.4815539780738618

对比这段代码:

>>> timeit.timeit("""s = s.replace("1","5")""", setup="s = 'hi 1 2 3'")
0.18738432400277816
>>> timeit.timeit("""if '1' in s: s = s.replace('1','5')""", setup="s = 'hi 1 2 3'")
0.02985000199987553
4

2 回答 2

5

我想我明白了。

看看这段代码:

timeit.timeit("""if '1' in s: s = s.replace('1','5')""", setup="s = 'hi 1 2 3'")

在此代码中,setup仅运行一次。这意味着它s变成了“全球”。结果,它hi 5 2 3在第一次迭代中被修改为,in现在False所有后续迭代返回。

请参阅此代码:

timeit.timeit("""if '1' in s: s = s.replace('1','5'); print(s)""", setup="s = 'hi 1 2 3'")

这将打印hi 5 2 3一次,因为它printif语句的一部分。对比一下,它会用大量的hi 5 2 3s 填满你的屏幕:

timeit.timeit("""s = s.replace("1","5"); print(s)""", setup="s = 'hi 1 2 3'")

所以这里的问题是带有测试的非函数有if缺陷并且给你错误的时间,除非你试图测试的是对已经处理的字符串的重复调用。(如果这是您尝试测试的内容,则您的函数版本存在缺陷。)函数 withif不能更好的原因是因为它replace在每次迭代的字符串的新副本上运行。

以下测试完成了我相信您的意图,因为它不会将replaceback的结果重新分配给 ,因此s每次迭代都不会对其进行修改:

>>> timeit.timeit("""if '1' in s: s.replace('1','5')""", setup="s = 'hi 1 2 3'"
0.3221409016812231
>>> timeit.timeit("""s.replace('1','5')""", setup="s = 'hi 1 2 3'")
0.28558505721252914

这个改动对我来说增加了很多if测试时间,也增加了一点非if测试时间,但是我使用的是 Python 2.7。但是,如果 Python 3 的结果是一致的,那么这些结果表明in当字符串很少需要任何替换时可以节省大量时间。如果它们通常确实需要更换,则似乎需要in花费一些时间。

于 2014-01-07T01:50:03.387 回答
0

通过查看反汇编代码变得更加奇怪。第二个块有版本(就像在 OP 的示例中一样,if它的时钟对我来说更快)。timeit

然而,通过查看操作码,它似乎纯粹有 7 个额外的操作码,从第一个开始BUILD_MAP,还涉及一个额外的操作码POP_JUMP_IF_TRUE(大概是为了if语句检查本身)。在此之前和之后,所有代码都相同。

这表明在if语句中构建和执行检查以某种方式减少了在调用中进行检查的计算时间replace。我们如何才能看到不同操作码的具体时序信息?

In [55]: dis.disassemble_string("s='HI 1 2 3'; s = s.replace('1','4')")
          0 POP_JUMP_IF_TRUE 10045
          3 PRINT_NEWLINE
          4 PRINT_ITEM_TO
          5 SLICE+2
          6 <49>
          7 SLICE+2
          8 DELETE_SLICE+0
          9 SLICE+2
         10 DELETE_SLICE+1
         11 <39>
         12 INPLACE_MODULO
         13 SLICE+2
         14 POP_JUMP_IF_TRUE 15648
         17 SLICE+2
         18 POP_JUMP_IF_TRUE 29230
         21 LOAD_NAME       27760 (27760)
         24 STORE_GLOBAL    25955 (25955)
         27 STORE_SLICE+0
         28 <39>
         29 <49>
         30 <39>
         31 <44>
         32 <39>
         33 DELETE_SLICE+2
         34 <39>
         35 STORE_SLICE+1

In [56]: dis.disassemble_string("s='HI 1 2 3'; if '1' in s: s = s.replace('1','4')")
          0 POP_JUMP_IF_TRUE 10045
          3 PRINT_NEWLINE
          4 PRINT_ITEM_TO
          5 SLICE+2
          6 <49>
          7 SLICE+2
          8 DELETE_SLICE+0
          9 SLICE+2
         10 DELETE_SLICE+1
         11 <39>
         12 INPLACE_MODULO
         13 SLICE+2
         14 BUILD_MAP        8294
         17 <39>
         18 <49>
         19 <39>
         20 SLICE+2
         21 BUILD_MAP        8302
         24 POP_JUMP_IF_TRUE  8250
         27 POP_JUMP_IF_TRUE 15648
         30 SLICE+2
         31 POP_JUMP_IF_TRUE 29230
         34 LOAD_NAME       27760 (27760)
         37 STORE_GLOBAL    25955 (25955)
         40 STORE_SLICE+0
         41 <39>
         42 <49>
         43 <39>
         44 <44>
         45 <39>
         46 DELETE_SLICE+2
         47 <39>
         48 STORE_SLICE+1
于 2014-01-06T23:27:04.530 回答