使用 timeit + exec 的奇怪行为

Strange behaviour using timeit + exec

我在尝试为 python 脚本计时时遇到了一些奇怪的行为。最小示例:

foobar.py:

foo = 'Hello'
print(''.join(c for c in foo if c not in 'World'))
print(''.join(c for c in 'World' if c not in foo))

timer.py:

import timeit
timeit.repeat(stmt="exec(open('foobar.py').read())", repeat=1, number=1)

当我 运行 foobar.py 时,我得到了预期的输出:

> python3 foobar.py 
He
Wrd

但是,当我 运行 timer.py 时,出现以下错误:

> python3 timer.py
He
Traceback (most recent call last):
  File "timer.py", line 2, in <module>
    timeit.repeat(stmt="exec(open('foobar.py').read())", repeat=1, number=1)
  File "/usr/lib/python3.7/timeit.py", line 237, in repeat
    return Timer(stmt, setup, timer, globals).repeat(repeat, number)
  File "/usr/lib/python3.7/timeit.py", line 204, in repeat
    t = self.timeit(number)
  File "/usr/lib/python3.7/timeit.py", line 176, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 6, in inner
  File "<string>", line 3, in <module>
  File "<string>", line 3, in <genexpr>
NameError: name 'foo' is not defined

也许最奇怪的是 foobar.py 中的第一个打印语句工作正常,而第二个却不行。在没有 timeit 包装器的情况下使用 exec 执行 foobar.py 也可以正常工作。

谁能解释这种奇怪的行为?

这实际上不限于 timeitexec 的结合,而是 exec 单独的问题:语句在本地命名空间中执行,生成器在 str.join 使用另一个(新的)本地命名空间,其中先前设置的 foo 未知。

Class definition blocks and arguments to exec() and eval() are special in the context of name resolution. A class definition is an executable statement that may use and define names. These references follow the normal rules for name resolution with an exception that unbound local variables are looked up in the global namespace. The namespace of the class definition becomes the attribute dictionary of the class. The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods – this includes comprehensions and generator expressions since they are implemented using a function scope. This means that the following will fail:

class A:
    a = 42
    b = list(a + i for i in range(10))

来源:https://docs.python.org/3/reference/executionmodel.html#resolution-of-names

另见 示例。

作为修复,您可以使用 exec 的第二个参数设置全局字典,因此所有语句都使用相同的字典:

timeit.repeat(stmt="exec(open('foobar.py').read(), locals())", repeat=1, number=1)

或者您可以完全删除 exec 并使用 import:

timeit.repeat(stmt="import foobar", repeat=1, number=1)

顺便说一下,直接执行 exec(open('foobar.py').read() 也仅当您在全局(模块)范围内时才有效。如果您只是将它放在一个函数中,它将停止工作并显示与 timeit.

调用时相同的错误