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

问题描述 投票:339回答:18

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()
python optimization coding-style
18个回答
242
投票

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

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

因此,如果您关心效率,请将进口放在首位。只有在你的分析显示有帮助的情况下才会将它们移动到一个函数中(你确实想要了解哪里可以提高性能,对吧?)


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

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

5
投票

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

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

4
投票

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

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


4
投票

只是为了完成Moe's answer和原始问题:

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

  1. 我们可以移动模块底部的一个from imports
  2. 我们可以在实际需要导入的函数或方法中移动其中一个from imports(这并不总是可行,因为您可以从几个地方使用它)。
  3. 我们可以将两个from imports中的一个改为导入,看起来像:import a

所以,总结一下。如果你没有处理循环依赖并做某种伎俩以避免它们,那么最好将所有导入放在顶部,因为这个问题的其他答案中已经解释过的原因。并且,当这个“技巧”包括评论时,它总是受欢迎的! :)


4
投票

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

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


3
投票

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

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


2
投票

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


1
投票

我很惊讶没有看到已经发布的重复负载检查的实际成本数字,尽管有很多很好的解释。

如果您在顶部导入,无论如何都会受到负载影响。这个很小,但通常是毫秒,而不是纳秒。

如果您在一个函数中导​​入,那么只有在首次调用其中一个函数时才会使用命中加载。正如许多人所指出的那样,如果根本没有发生这种情况,则可以节省加载时间。但是如果函数被调用了很多,你会重复一次,但要检查它是否已被加载;不是为了实际重新加载。另一方面,正如@aaronasterling指出的那样,你也节省了一点,因为在一个函数中导​​入让函数使用稍快一些的局部变量查找来稍后识别名称(http://stackoverflow.com/questions/477096/python-import-coding-style/4789963#4789963)。

以下是从函数内部导入一些内容的简单测试的结果。报告的时间(在2.3 GHz英特尔酷睿i7上的Python 2.7.14中)如下所示(第二次呼叫比以后的呼叫看起来更加一致,但我不知道为什么)。

 0 foo:   14429.0924 µs
 1 foo:      63.8962 µs
 2 foo:      10.0136 µs
 3 foo:       7.1526 µs
 4 foo:       7.8678 µs
 0 bar:       9.0599 µs
 1 bar:       6.9141 µs
 2 bar:       7.1526 µs
 3 bar:       7.8678 µs
 4 bar:       7.1526 µs

代码:

from __future__ import print_function
from time import time

def foo():
    import collections
    import re
    import string
    import math
    import subprocess
    return

def bar():
    import collections
    import re
    import string
    import math
    import subprocess
    return

t0 = time()
for i in xrange(5):
    foo()
    t1 = time()
    print("    %2d foo: %12.4f \xC2\xB5s" % (i, (t1-t0)*1E6))
    t0 = t1
for i in xrange(5):
    bar()
    t1 = time()
    print("    %2d bar: %12.4f \xC2\xB5s" % (i, (t1-t0)*1E6))
    t0 = t1

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


0
投票

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

Optional Imports

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

通过这种方式,我可以执行“restart-and-run-all”而无需等待导入,或者在失败时必须恢复其余的单元格。


71
投票

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


55
投票

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

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

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

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

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


34
投票

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

首先,您可以使用具有以下形式的单元测试的模块:

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

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


14
投票

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

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

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


8
投票

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

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

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


8
投票

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

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

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

例如:

do_something_with_x(x)

我可以调试这个:

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

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

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


6
投票

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

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

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

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

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


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说的是什么。)

© www.soinside.com 2019 - 2024. All rights reserved.