导入语句是否应该始终位于模块的顶部?

时间:2020-03-06 14:40:34  来源:igfitidea点击:

PEP 08指出:

Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants.

但是,如果仅在极少数情况下使用我要导入的类/方法/函数,那么在需要时进行导入肯定会更有效吗?

这不是吗?

class SomeClass(object):

    def not_often_called(self)
        from datetime import datetime
        self.datetime = datetime.now()

比这更有效?

from datetime import datetime

class SomeClass(object):

    def not_often_called(self)
        self.datetime = datetime.now()

解决方案

当函数被调用零次或者一次时,第一种变体的确比第二种变体更有效。但是,在第二次及其后的调用中,"导入每个调用"方法实际上效率较低。请参阅此链接以获取延迟加载技术,该技术通过执行"延迟导入"结合了两种方法的优点。

但是,除了效率以外,还有其他原因可能导致我们更喜欢一个而不是另一个。一种方法是使某些人更清楚地阅读该模块所具有的依赖关系的代码。它们还具有非常不同的故障特征-如果没有" datetime"模块,第一个将在加载时失败,而第二个在调用该方法之前不会失败。

补充说明:在IronPython中,导入可能比CPython中昂贵得多,因为代码基本上是在导入时进行编译的。

我不会担心过多地预先加载模块的效率。模块占用的内存不会很大(假设它足够模块化),启动成本可以忽略不计。

在大多数情况下,我们希望将模块加载到源文件的顶部。对于阅读代码的人来说,它更容易分辨出哪个功能或者对象来自哪个模块。

在代码的其他位置导入模块的一个很好的理由是,如果该模块在调试语句中使用过。

例如:

do_something_with_x(x0

我可以使用以下命令调试它:

from pprint import pprint
pprint(x)
do_something_with_x(x)

当然,在代码中其他地方导入模块的另一个原因是是否需要动态导入它们。这是因为我们几乎别无选择。

我不会担心过多地预先加载模块的效率。模块占用的内存不会很大(假设它足够模块化),启动成本可以忽略不计。

在大多数情况下,这样做对于保持清晰性和明智性很有用,但并非总是如此。以下是几个可能会在其他地方导入模块的情况的示例。

首先,我们可以拥有一个带有以下形式的单元测试的模块:

if __name__ == '__main__':
    import foo
    aa = foo.xyz()         # initiate something for the test

其次,我们可能需要在运行时有条件地导入一些不同的模块。

if [condition]:
    import foo as plugin_api
else:
    import bar as plugin_api
xx = plugin_api.Plugin()
[...]

在其他情况下,我们可能会将导入放置在代码的其他部分中。

这是一个折衷,只有程序员才能决定进行。

情况1通过在需要之前不导入datetime模块(并进行可能需要的任何初始化)来节省一些内存和启动时间。请注意,"仅在调用时"执行导入也意味着"在调用时每次"执行导入操作,因此第一个调用之后的每个调用仍会产生执行导入的额外开销。

情况2通过预先导入datetime来节省一些执行时间和延迟,这样not_often_drawn()在被调用时将更快地返回,并且还不会在每次调用时都导致导入开销。

除了效率外,如果import语句在...前面,则更容易预先查看模块的依赖性。将它们隐藏在代码中可能使更容易找到依赖的模块变得更加困难。

就个人而言,除了单元测试之类的东西外,我通常都遵循PEP,因此我不希望总是加载它,因为我知道除了测试代码之外不会使用它们。

Curt提出了一个很好的观点:第二个版本更清晰,它将在加载时失败,而不是在以后出现意外。

通常,我不必担心模块的加载效率,因为它的运行速度(a)非常快,而(b)大多仅在启动时发生。

如果必须在非预期的时间加载重量级模块,则最好使用__import__函数动态加载它们,并确保捕获到" ImportError"异常,并以合理的方式处理它们。

模块导入非常快,但不是即时的。这意味着:

  • 将导入放在模块顶部很好,因为这是微不足道的成本,只需要支付一次即可。
  • 将导入放在函数中会导致对该函数的调用花费更长时间。

因此,如果我们关心效率,则将进口放在首位。仅在分析显示有帮助的情况下,才将它们移入功能(我们进行了剖析以查看最能改善性能的地方,对吗?)

我见过执行延迟导入的最佳原因是:

  • 可选的库支持。如果代码具有使用不同库的多个路径,则在未安装可选库的情况下不要中断。
  • 在插件的__init__.py中,可能已导入但并未实际使用。例如Bazaar插件,它使用bzrlib的延迟加载框架。

这是一个示例,其中所有导入都位于最顶部(这是我唯一需要这样做的时间)。我希望能够在Un * x和Windows上终止子进程。

import os
# ...
try:
    kill = os.kill  # will raise AttributeError on Windows
    from signal import SIGTERM
    def terminate(process):
        kill(process.pid, SIGTERM)
except (AttributeError, ImportError):
    try:
        from win32api import TerminateProcess  # use win32api if available
        def terminate(process):
            TerminateProcess(int(process._handle), -1)
    except ImportError:
        def terminate(process):
            raise NotImplementedError  # define a dummy function

(评论:约翰·米利金说的话。)

就像许多其他优化一样,我们为了速度而牺牲了一些可读性。正如John所提到的,如果我们完成了配置作业,并发现这是一项非常有用的更改,并且我们需要额外的速度,那么就去做。最好在所有其他进口商品上加上注释:

from foo import bar
from baz import qux
# Note: datetime is imported in SomeClass below

模块初始化仅在第一次导入时发生一次。如果有问题的模块来自标准库,那么我们也可能会从程序中的其他模块导入它。对于像日期时间这样流行的模块,它也可能是许多其他标准库的依赖项。由于模块初始化已经发生,所以import语句的花费很少。此时,它所做的全部工作就是将现有模块对象绑定到本地作用域。

将该信息与用于可读性的参数相结合,我想说最好在模块范围内使用import语句。

我采用了将所有导入放入使用它们的函数中而不是放在模块顶部的做法。

我得到的好处是能够更可靠地进行重构。当我将一个功能从一个模块移动到另一个模块时,我知道该功能将继续使用其完整的测试遗留功能。如果我在模块顶部放置了导入文件,那么当我移动一个函数时,我发现我最终花了很多时间来使新模块的导入文件完整而最少。重构IDE可能与此无关。

如其他地方提到的那样,存在速度损失。我已经在我的应用程序中对此进行了测量,发现对于我的目的而言它是微不足道的。

能够预先查看所有模块依赖项而无需借助搜索(例如grep)也是很好的。但是,我关心模块依赖性的原因通常是因为我正在安装,重构或者移动包含多个文件的整个系统,而不仅仅是一个模块。在这种情况下,无论如何,我将执行全局搜索以确保我具有系统级依赖项。因此,我还没有发现全局导入可以帮助我在实践中理解系统。

我通常将sys的导入放入if if name __ =='main__'检查中,然后将参数(例如sys.argv [1:])传递给main()函数。这使我可以在尚未导入sys的上下文中使用main。

将import语句放在函数内部可以防止循环依赖。
例如,如果我们有两个模块X.py和Y.py,并且它们都需要互相导入,那么当我们导入其中一个模块导致无限循环时,这将导致循环依赖。如果将import语句移动到一个模块中,则它将在调用该函数之前不会尝试导入另一个模块,并且该模块已经被导入,因此不会出现无限循环。在此处阅读更多信息effbot.org/zone/import-confusion.htm