在 timeit.repeat 中重置全局变量

Reset global variables in timeit.repeat

场景

test 成为我们 运行 作为 __main__ 的模块。该模块包含一个名为 primes 的全局变量,该变量在模块中使用以下赋值进行初始化。

primes = []

该模块还包含一个名为 pi 的函数,它改变了这个全局变量:

def pi(n):
    global primes
    """Some code that modifies the global 'primes' variable"""

然后我想使用内置的 timeit 模块为所述函数计时。我想使用 timeit.repeat 函数并获得时间的最小值,作为提高测量精度的一种方式(而不是只测量一次,因为不相关的过程可能会减慢速度)。

print(min(timeit.repeat('test.pi(50000)',
                        setup="import test",
                        number=1, repeat=10)) * 1000)

问题是 pi 函数的行为因 primes 的值而异:我预计,对于每次重复,setup 中的 import test 语句] 参数将重新 运行 test 中的 primes = [] 语句,因此 'resetting' primes 以便每次重复执行的代码都是相同的。但是,取而代之的是,使用了上次执行的结果 primes 的值,因此我不得不将语句 test.primes = [] 添加到 setup 参数中:

print(min(timeit.repeat('test.pi(50000)',
                        setup="import test \n" + "test.primes = []",
                        number=1, repeat=10)) * 1000)

问题

这让我想到了一个问题:是否有一种直接的方法(即在一个语句中)'reset' all 全局变量的值到它们原来的值他们是什么时候第一次在模块中分配的?

在这个特定的场景中手动添加一个语句 'reset' primes 工作正常,但考虑一个有很多全局变量的情况,你想 'reset' 全部。


支线问题

为什么语句 import test 不重新 运行 初始 primes = [] 赋值?

让我们从你的附带问题开始,因为事实证明它实际上是一切的核心:

Why doesn't the statement import test re-run the initial primes = [] assignment?"

因为,正如 the import system and the import statement 上的文档中所解释的,import test 所做的大致是这个伪代码:

if 'test' not in sys.modules:
    find, load (compiling if needed), and exec the module
    sys.modules['test'] = result
test = sys['test.modules']

好的,但是为什么它会那样做?

  • 如果您有两个模块都导入相同的模块,它们希望看到相同的全局变量。请记住,在函数顶层定义的类型、函数等都是全局变量。例如,如果 sortedlist.pycollections.abc 导入到 class SortedList(collections.abc.Sequence):,并且 scraper.pycollections.abc 导入到 isinstance(something, collections.abc.Sequence),您需要 SortedList 才能通过该测试——但如果它们是两个完全独立的类型则不会,因为它们来自两个恰好具有相同名称的不同模块对象,

  • 如果您有 12 个模块,全部 import pandas as pd,您将 运行 将所有 Pandas 初始化代码编译 12 次。除了你的一些模块也可能相互导入,所以它们每个都是 运行 多次,并且每次都导入 Pandas。您认为 运行 所有 Pandas 初始化 60 次需要多长时间?


因此,重用现有模块几乎总是您想要的。

如果您不这样做,通常表明您的设计存在问题(这里很可能就是这种情况)。

但是 "almost always" 不是 "always"。所以有办法解决它。 None 其中对于实时代码通常是个好主意,但对于单元测试和基准测试之类的事情,只要权衡是您想要的,就可以使用三个基本选项:

  • del sys.modules['test']。这显然很老套,但它实际上完全符合您的要求。对旧模块的任何现有引用都完全保持不变,但下次有人这样做时 import test,他们将获得一个全新的 test 模块。
  • importlib.reload(test)。这听起来不错,但一方面可能有点矫枉过正(请注意,它会强制重新编译模块源代码,这是您不需要的),而另一方面可能还不够(它实际上并没有重置globals——如果你的代码在顶层执行 primes = [],那一行就会被执行,所以谁在乎,但是如果你的代码在 pi 函数中执行 globals().setdefault('primes', []),你就会在乎).
  • 而不是 import test,通过执行模块手动完成所有步骤(请参阅 importlib 文档中的 examples),但不要将其存储在 sys.modules['test']test 中,只需将其存储在每次测试后丢弃的局部变量中。这可能是最干净的,尽管它确实意味着 6 行代码而不是 1 行。