你能使用闭包在函数中优化导入吗?

7

这两个问题涉及在函数内部使用import与在模块顶部使用import。我不需要被说服将我的导入放在顶部,有很好的理由这样做。然而,为了更好地理解技术问题,我想提出一个后续问题。

通过使用闭包并仅在第一次运行函数时进行导入,您是否可以在性能方面获得最佳效果?


具体而言,假设您有如下代码:

import sys
def get_version():
    return sys.version

您希望只有在调用函数时才发生导入,因此将其移至函数内部:

def get_version():
    import sys
    return sys.version

但是如果它被频繁调用,现在会变得很慢,因此您尝试了一些更复杂的东西:

def _get_version():
    import sys

    def nested():
        return sys.version

    global get_version
    get_version = nested
    return nested()
get_version = _get_version

现在至少一个基本的性能测试表明,这个最后的选项比第一个选项稍微慢一些(需要大约110%的时间),但比第二个选项快得多(只需要大约20%的时间)。


首先,这是否真的有效?我的测量是否准确地描述了第二个示例执行更多工作,还是这只是我测量方式的产物。

其次,在闭包中是否存在减速-除了第一次运行函数的时间之外?


如果是同级函数,您可以使用 functools 中的 lru_cache 装饰器。不确定是否是一个好主意,但它会在第一次运行后缓存函数的结果。 - sytech
1
@sytech:lru_cache缓存测试非常耗费资源,我怀疑对于像导入这样轻量级的操作,它不会带来太多性能提升(一旦完成初始文件加载后,导入操作只是绑定一个名称而已)。 - Martijn Pieters
1个回答

3

闭包解引用并不比全局查找更快:

>>> import sys
>>> sys.version_info
sys.version_info(major=3, minor=6, micro=0, releaselevel='final', serial=0)
>>> from timeit import timeit
>>> glob = 'foo'
>>> def f1(): return glob
...
>>> def closure():
...     closed_over = 'bar'
...     def f2():
...         return closed_over
...     return f2
...
>>> f2 = closure()
>>> timeit(f1, number=10**7)
0.8623221110319719
>>> timeit(f2, number=10**7)
0.872071701916866

此外,即使它更快,与可读性之间的权衡是不值得的,当您真正需要优化代码时,肯定有更快的选项可用。

如果您确实需要优化从紧密循环调用的代码,本地变量始终是最快的选择,适当的混合方式是使用函数参数默认值:

import sys.version

def get_version(_sys_version=sys.version):
    return _sys_version

如果您担心在启动时从导入的初始文件加载对性能的影响,也许您应该考虑使用 py-demandimport 项目,该项目会推迟加载模块直到它们第一次被使用。

1
是的,就像我在问题中写的那样,我的测量结果表明它稍微慢了一些。重点是优化速度,同时保留仅在调用时导入的属性,而您的函数参数示例缺少此功能。 - otus
这只是假设,但 py-demandimport 项目似乎是我实际可以使用的东西。谢谢。 - otus

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