import语句应该始终位于模块的顶部吗?

时间:2008-09-24 17:21:48

标签: python optimization coding-style

PEP 08州:

  

导入总是放在文件的顶部,就在任何模块注释和文档字符串之后,以及模块全局变量和常量之前。

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

不是这个:

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()

19 个答案:

答案 0 :(得分:246)

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

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

因此,如果您关心效率,请将进口放在首位。只有在您的分析显示有帮助时才会将它们移动到一个函数中(您执行配置文件以查看最佳位置以改善性能,对吧?)


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

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

答案 1 :(得分:71)

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

答案 2 :(得分:55)

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

我获得的好处是能够更可靠地重构。当我将一个函数从一个模块移动到另一个模块时,我知道该函数将继续使用它的所有遗留测试。如果我在模块的顶部有我的导入,当我移动一个函数时,我发现我最终花了很多时间让新模块的导入完成且最小化。重构IDE可能会使这无关紧要。

如其他地方所述,存在速度惩罚。我已经在我的申请中对此进行了测量,发现它对我的目的来说是微不足道的。

能够预先查看所有模块依赖关系而不诉诸搜索(例如grep)也很好。但是,我关心模块依赖性的原因通常是因为我正在安装,重构或移动包含多个文件的整个系统,而不仅仅是单个模块。在这种情况下,我将要执行全局搜索以确保我具有系统级依赖性。所以我没有找到全球进口来帮助我理解系统的实际情况。

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

答案 3 :(得分:36)

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

首先,您可以拥有一个模块,其中包含以下形式的单元测试:

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()
[...]

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

答案 4 :(得分:14)

当函数被调用为零或一次时,第一个变体确实比第二个更有效。但是,通过第二次和后续调用,“导入每次调用”方法实际上效率较低。有关延迟加载技术,请参阅this link,该技术通过执行“延迟导入”来结合两种方法中的最佳方法。

但是除了效率之外还有其他原因,为什么你们可能更喜欢一个而不是另一个。一种方法是让读取代码的人更清楚该模块具有的依赖关系。它们也有非常不同的故障特征 - 如果没有“datetime”模块,第一个将在加载时失败,而第二个在调用方法之前不会失败。

添加注意:在IronPython中,导入可能比CPython昂贵得多,因为代码基本上是在导入时编译的。

答案 5 :(得分:8)

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

在大多数情况下,您希望在源文件的顶部加载模块。对于读取代码的人来说,它可以更容易地告诉哪个函数或对象来自哪个模块。

在代码中的其他位置导入模块的一个很好的理由是它是否在调试语句中使用。

例如:

do_something_with_x(x)

我可以用以下方法调试:

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

当然,在代码中的其他位置导入模块的另一个原因是您需要动态导入它们。这是因为你几乎没有任何选择。

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

答案 6 :(得分:8)

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

通常我不担心加载模块的效率,因为它(a)非常快,(b)大部分只发生在启动时。

如果您必须在意外时间加载重量级模块,使用__import__函数动态加载它们可能更有意义,并且确定以捕获ImportError例外,并以合理的方式处理它们。

答案 7 :(得分:6)

这是一个示例,其中所有导入都在最顶层(这是我唯一需要这样做的时间)。我希望能够在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 Millikin说的是什么。)

答案 8 :(得分:6)

这就像许多其他优化一样 - 你牺牲了一些速度的可读性。正如约翰所提到的,如果你已经完成了你的剖析作业并发现这是一个非常有用的变化你需要额外的速度,那就去吧。把所有其他进口产品都记下来可能会很好:

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

答案 9 :(得分:6)

这是一种权衡,只有程序员才能决定。

案例1通过不导入datetime模块(并执行可能需要的任何初始化)来节省一些内存和启动时间,直到需要时为止。请注意,仅在调用时执行导入也意味着“每次调用时”执行此操作,因此每次调用后的每次调用仍然会产生执行导入的额外开销。

案例2通过预先导入日期时间来节省一些执行时间和延迟,这样当 调用时,not_often_called()将更快地返回,并且不会在每次调用时产生导入的开销。

除了效率之外,如果导入语句是预先的,那么预先更容易看到模块依赖性。将它们隐藏在代码中会使得更容易找到某些依赖的模块。

我个人一般都遵循PEP,除了像单元测试这样的东西,我不想总是加载,因为我知道除测试代码外不会使用它们。

答案 10 :(得分:4)

模块初始化仅发生一次 - 在第一次导入时。如果有问题的模块来自标准库,那么您也可以从程序中的其他模块导入它。对于像datetime一样流行的模块,它也可能是许多其他标准库的依赖项。由于模块初始化已经发生,因此import语句的成本非常低。此时所做的只是将现有模块对象绑定到本地范围。

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

答案 11 :(得分:4)

只需完成Moe's answer和原始问题:

当我们必须处理循环依赖时,我们可以做一些“技巧”。假设我们正在使用包含{{1}的模块a.pyb.py分别是}和b x()。然后:

  1. 我们可以移动模块底部的y()之一。
  2. 我们可以移动实际需要导入的函数或方法中的from imports之一(这并不总是可行的,因为您可以从多个地方使用它)。
  3. 我们可以将两个from imports中的一个更改为导入,如下所示:from imports
  4. 所以,总结一下。如果你没有处理循环依赖并做某种伎俩以避免它们,那么最好将所有导入放在顶部,因为这个问题的其他答案中已经解释过的原因。并且,当这个“技巧”包括评论时,它总是受欢迎的! :)

答案 12 :(得分:4)

除了已经给出的优秀答案之外,值得注意的是进口的放置不仅仅是风格问题。有时,模块具有需要首先导入或初始化的隐式依赖项,并且顶级导入可能会导致违反所需的执行顺序。

这个问题经常出现在Apache Spark的Python API中,您需要在导入任何pyspark软件包或模块之前初始化SparkContext。最好将pyspark导入放置在保证SparkContext可用的范围内。

答案 13 :(得分:3)

我不想提供完整的答案,因为其他人已经做得很好。我只想提一个用例,当我发现在函数内导入模块时特别有用。我的应用程序使用python包和存储在特定位置的模块作为插件。在应用程序启动期间,应用程序遍历该位置的所有模块并导入它们,然后它查看模块内部,如果它找到插件的一些安装点(在我的情况下,它是某个基类具有唯一的子类) ID)它注册它们。插件的数量很大(现在很多,但未来可能有数百个),而且很少使用它们。在我的插件模块顶部导入第三方库在应用程序启动期间有点受到惩罚。特别是一些第三方库很难导入(例如,即使尝试连接到互联网并下载一些在启动时添加大约一秒的内容,也会导致。通过在插件中优化导入(仅在它们使用的函数中调用它们),我设法将启动从10秒缩小到大约2秒。这对我的用户来说是一个很大的不同。

所以我的答案是否定的,不要总是将导入放在模块的顶部。

答案 14 :(得分:2)

有趣的是,到目前为止,没有一个答案提到了并行处理,当序列化的功能代码被推送到其他核心时,可能需要导入在函数中。就像ipyparallel一样。

答案 15 :(得分:2)

可读性

除了启动性能外,还有一个可读性参数可用于本地化import语句。例如,在我当前的第一个python项目中,使用python行号1283到1296:

listdata.append(['tk font version', font_version])
listdata.append(['Gtk version', str(Gtk.get_major_version())+"."+
                 str(Gtk.get_minor_version())+"."+
                 str(Gtk.get_micro_version())])

import xml.etree.ElementTree as ET

xmltree = ET.parse('/usr/share/gnome/gnome-version.xml')
xmlroot = xmltree.getroot()
result = []
for child in xmlroot:
    result.append(child.text)
listdata.append(['Gnome version', result[0]+"."+result[1]+"."+
                 result[2]+" "+result[3]])

如果import语句位于文件顶部,则必须向上滚动很长一段距离,或者按 Home 来找出ET是什么。然后,我将不得不导航回到第1283行以继续阅读代码。

实际上,即使import语句位于函数(或类)的顶部(如可能放置的很多),也需要向上和向下分页。

很少会显示Gnome版本号,因此文件顶部的import会引起不必要的启动延迟。

答案 16 :(得分:1)

通过在函数内部导入变量/局部作用域可以提高性能。这取决于函数中导入事物的用法。如果要循环很多次并访问模块全局对象,则将其作为本地导入可以有所帮助。

test.py

X=10
Y=11
Z=12
def add(i):
  i = i + 10

runlocal.py

from test import add, X, Y, Z

    def callme():
      x=X
      y=Y
      z=Z
      ladd=add 
      for i  in range(100000000):
        ladd(i)
        x+y+z

    callme()

run.py

from test import add, X, Y, Z

def callme():
  for i in range(100000000):
    add(i)
    X+Y+Z

callme()

在Linux上使用一段时间显示收益很小

/usr/bin/time -f "\t%E real,\t%U user,\t%S sys" python run.py 
    0:17.80 real,   17.77 user, 0.01 sys
/tmp/test$ /usr/bin/time -f "\t%E real,\t%U user,\t%S sys" python runlocal.py 
    0:14.23 real,   14.22 user, 0.01 sys

真实是壁钟。用户正在编程。 sys是时候进行系统调用了。

https://docs.python.org/3.5/reference/executionmodel.html#resolution-of-names

答案 17 :(得分:0)

我想提一下我的一个用例,它与@John Millikin和@ V.K提到的用例非常相似。 :

可选导入

我使用Jupyter Notebook进行数据分析,并且使用相同的IPython Notebook作为所有分析的模板。在某些情况下,我需要导入Tensorflow来进行一些快速的模型运行,但有时我会在未设置tensorflow或导入缓慢的地方工作。在这些情况下,我将与Tensorflow相关的操作封装在一个辅助函数中,将tensorflow导入该函数内部,并将其绑定到按钮。

这样,我可以“重新启动并运行所有程序”,而不必等待导入,也不必在导入失败时恢复其余的单元格。

答案 18 :(得分:0)

这是一个有趣的讨论。像许多其他人一样,我什至从未考虑过这个话题。由于想要在我的一个库中使用Django ORM,我不得不在函数中具有导入功能。导入模型类之前,我不得不调用django.setup(),并且由于它位于文件的顶部,由于IoC注入器的构造,它被拖到了完全非Django的库代码中。

我有点脚,最终将django.setup()放在单例构造函数中,并将相关的导入放在每个类方法的顶部。现在,这种方法工作正常,但是由于进口商品不在顶部而使我感到不安,而且我也开始担心进口商品的额外时间。然后我来到这里,饶有兴趣地阅读了大家对此的看法。

我有很长的C ++背景,现在使用Python / Cython。我对此的看法是,为什么不将导入内容放入函数中,除非它导致概要分析的瓶颈。这就像在需要变量之前声明空间。麻烦的是,我有数千行代码,所有导入都在顶部!因此,我认为从现在开始,我将在有时间的时候在此更改奇数文件。