【问题标题】:Reset global variables in timeit.repeat在 timeit.repeat 中重置全局变量
【发布时间】:2018-08-27 20:49:34
【问题描述】:

场景

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 语句将重新运行 primes = [] test 中的语句,因此“重置”primes 以便每次重复执行的代码都是相同的。但是,取而代之的是使用上一次执行产生的primes 的值,所以我不得不将语句test.primes = [] 添加到setup 参数中:

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

问题

这让我想到了一个问题:是否有直接的方法(即在一个语句中)将 all 全局变量的值“重置”为它们在模块?

在这种特定情况下,添加一条语句以手动“重置”primes 可以正常工作,但考虑有很多全局变量的情况,并且您想要“重置”所有变量。


支线问题

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

【问题讨论】:

  • "但是考虑一个有很多全局变量的情况,你想'重置'所有变量。"那只是糟糕的程序设计。我建议您编写代码,使其不依赖于可变的全局状态,这是一种经典的反模式。
  • 但是如果你坚持设计不佳,你可以使用import importlib; importlib.reload('test')
  • 如果您询问在外部“重复”循环或单独的 timeit 调用之间保留的全局变量,那么您的问题是您希望 import 进行干净的重新导入,而您可以通过 del sys.imports['test'] 轻松地做到这一点,或者通过编写一个手动导入和准备 test.py 而不首先将其添加到 sys.modules 的 5 行函数来干净地做到这一点。
  • @juanpa.arrivillaga 我不认为你想要reload。这将强制重新编译源代码,这通常只是不必要的开销。此外,它实际上并没有重置全局变量——如果有执行primes = [] 的顶级模块代码,则该代码将重新运行,所以谁在乎……但如果测试函数中有一些可怕的代码执行类似的操作globals().setdefault('primes', [])(考虑到这个问题,我不会立即驳回……),它不会。
  • @abarnert 谢谢。我想如果我想编写一个不添加到sys.modules 的自定义“导入函数”,我会在您链接的文档中找到所需的资源?

标签: python timing timeit


【解决方案1】:

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

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

因为,正如 the import systemthe 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 次需要多长时间?


所以,重用现有模块几乎总是你想要的。

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

但“几乎总是”不是“总是”。所以有办法解决它。对于实时代码而言,它们通常都不是一个好主意,但对于单元测试和基准测试之类的事情,有三个基本选项都可以,只要权衡是你想要的:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-05
    • 2013-04-18
    • 2015-12-04
    • 2016-03-15
    • 2016-07-13
    • 2023-03-04
    • 1970-01-01
    相关资源
    最近更新 更多