使用内联导入可能有什么负面影响吗?
我正在开发一个Python程序,希望即使缺少某些库,它也能正常运行。这些库是程序某些功能所需要的。(编辑:我写了一些代码来实现最佳建议的解决方案,代码可以在这里找到,测试代码在这里。)
我通过把需要的库的导入语句放在使用它们的函数内部,而不是在Python文件的顶部,来解决这个问题。这意味着即使你没有这些库,文件也能正常加载,不过如果你尝试调用其中一个函数,就会出现导入错误(ImportError)。
这个方法效果很好,以至于我有时也会对标准库模块这样做——但现在我在想,这样做会不会有什么隐藏的成本呢?
基础代码:
import numpy
def foo():
return numpy.array([])
def bar():
return numpy.array([1, 2, 3])
带有内联导入的代码:
def foo():
import numpy
return numpy.array([])
def bar():
import numpy
return numpy.array([1, 2, 3])
编辑:
我完全同意不应该对标准库代码进行内联导入——这显然是不好的做法。
我现在认为,使用受保护的导入是正确的解决方案。
特别是,我对调用进行了时间测试,虽然对于大多数应用来说,时间差异可能不大,但还是能感受到(我知道这是个微妙的界限!)
在一个简单的情况下:
import numpy
def f():
return numpy
在我的机器上,执行100,000次大约需要180毫秒,而
def f():
import numpy
return numpy
则大约需要870毫秒。
粗略的结论是,这样的成本相当于四次简单函数调用——在大多数情况下是明显的,但并不算重要。不过,如果这样做不会给你带来任何成本,最好还是避免。
在实验中,我还意识到内联导入的另一个缺点——这些导入是在函数被调用时以不可预测的时间发生的。在我的应用中,涉及实时元素,这种情况是不可接受的。
4 个回答
其实不是这样的。
导入操作只会发生一次,但可能在用户意想不到的时刻发生,也就是当调用那个进行导入的函数时,第一次才会执行。
另外,这也是为了代码的可读性——如果你按照惯例把导入放在最上面,任何阅读你代码的人都能立刻知道它依赖了哪些东西。如果导入发生在你模块的第284行,这种清晰度可能就会丢失了……
你这样做是不符合PEP 8规范的。对于标准库的导入,你没有充分的理由这样做,这样做是双重错误,可能会让一些人不愿意使用你的代码(或者至少会礼貌地提醒你不要这样做)。
当然,PEP 8的规定不是没有原因的。在这种情况下,有一个比个人喜好和统一性更好的理由:如果把所有的导入放在文件的顶部,别人就能很容易找到一个模块的依赖关系。如果导入分散在文件的各个地方,这就会变得麻烦多了。而且,现在几乎每次调用你的库时都有可能出现ImportError
错误,这就很不幸了:通常的工作流程是先导入所有东西,如果能成功导入,就假设它能正常工作(这在设置虚拟环境时是一个有用的手动测试)。如果代码写得不太好,可能会在执行输入输出操作时,突然调用你的函数(没想到会出现ImportError
),然后就会对错误感到惊讶,导致无法正确清理。
还有一点小开销,就是每次调用一个包含导入的函数时,会执行一些额外的指令。不过,这种开销对于大多数情况来说是相对较小的,而且它不会重复导入模块(无论是两次、三次还是多次)。当然,这样做也违反了DRY原则。
面对这个问题,我和其他人通常选择把导入放在文件的顶部,并用try: ... except ImportError:
包裹起来。这样你可以给一个虚拟值,发出警告,记录一些信息,或者做其他在你情况下合理的事情。你甚至可以导入一个替代模块(例如,当支持某些没有特定模块的旧Python版本时)或者你自己提供的一个占位模块。
这样做不会明显影响性能,但会让你的代码变得杂乱。如果你决定添加一个新的导入,或者需要更改一个旧的导入,你就得到处修改,而不是只在一个地方改。
另外,你要确保这点有记录。有些用户可能会感到不满,因为库看起来导入得很正常,但在调用某个特定函数时却失败了。此外,虽然整体性能没有下降,但可能会出现性能“重组”,导致在意想不到的地方变慢。第一次调用一个导入了 numpy
的函数时,它需要进行导入,这会花费一些时间。用户可能会觉得这样不好,想要把所有慢的导入都提前完成。
你可以通过把所有导入放在顶部来轻松实现类似的效果:
try:
import numpy
except ImportError:
warnings.warn("Numpy not available, some functions may not work!")
之后尝试使用那些试图访问 numpy
的函数时,就会出现 NameError 错误。通过使用警告(或者只是打印/记录一条消息),你可以提前通知用户某些功能将无法使用,而不是让他们在后面突然遇到问题。