Should import statements always be at the top of a module?
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.
但是,如果我要导入的类/方法/函数仅在少数情况下使用,那么在需要时执行导入确实更有效?
这不是吗?
1 2 3 4 5 | class SomeClass(object): def not_often_called(self) from datetime import datetime self.datetime = datetime.now() |
比这更有效率?
1 2 3 4 5 6 | from datetime import datetime class SomeClass(object): def not_often_called(self) self.datetime = datetime.now() |
模块导入非常快,但不是即时的。这意味着:
- 把导入放在模块的顶部是可以的,因为这是一个只需支付一次的微不足道的成本。
- 将导入放到函数中会导致对该函数的调用花费更长的时间。
所以如果你关心效率,把进口放在首位。只有当您的分析显示这有帮助时,才将它们移动到一个函数中(您进行了分析以查看哪里是提高性能的最佳方法,对吗??)
我看到执行延迟导入的最佳原因是:
- 可选的库支持。如果您的代码有多个使用不同库的路径,请不要在没有安装可选库的情况下中断。
- 在插件的
__init__.py 中,该插件可能被导入,但实际上并未使用。例如Bazaar插件,它使用bzrlib 的惰性加载框架。
将import语句放入函数内部可以防止循环依赖。例如,如果您有两个模块(x.py和y.py),并且它们都需要相互导入,那么当您导入其中一个模块导致无限循环时,这将导致循环依赖。如果您在其中一个模块中移动import语句,那么在调用函数之前,它不会尝试导入另一个模块,并且该模块已经被导入,因此没有无限循环。阅读此处了解更多信息-effbot.org/zone/import-confusion.htm
我采用了将所有导入放入使用它们的函数中的做法,而不是放在模块的顶部。
我得到的好处是能够更可靠地重构。当我将一个函数从一个模块移动到另一个模块时,我知道该函数将继续完整地使用其所有遗留测试。如果我在模块的顶部有导入,当我移动一个函数时,我会发现我会花费大量的时间来完成新模块的导入并将其最小化。重构IDE可能会使这变得不相关。
其他地方也提到了速度惩罚。我已经在我的应用程序中对它进行了测量,发现它对于我的目的来说无关紧要。
在不使用搜索(例如grep)的情况下,能够提前看到所有模块依赖项也是一件好事。但是,我关心模块依赖性的原因通常是因为我正在安装、重构或移动一个包含多个文件的整个系统,而不仅仅是一个模块。在这种情况下,我无论如何都要执行全局搜索,以确保我有系统级的依赖关系。因此,我没有找到全球进口来帮助我在实践中理解一个系统。
我通常将
大多数时候,这对于清晰和明智的做法是有用的,但情况并非总是如此。下面是几个模块导入可能位于其他地方的情况示例。
首先,您可以拥有一个具有以下形式的单元测试的模块:
1 2 3 | if __name__ == '__main__': import foo aa = foo.xyz() # initiate something for the test |
其次,您可能需要在运行时有条件地导入一些不同的模块。
1 2 3 4 5 6 | if [condition]: import foo as plugin_api else: import bar as plugin_api xx = plugin_api.Plugin() [...] |
在其他情况下,您可能会在代码的其他部分放置导入。
当函数被调用零次或一次时,第一个变量实际上比第二个变量更有效。然而,对于第二次和随后的调用,"导入每个调用"方法实际上效率较低。请参阅此链接,了解通过执行"惰性导入"将两种方法中的最佳方法组合在一起的惰性加载技术。
但除了效率之外,还有其他原因让你更喜欢一个。一种方法是让阅读代码的人更清楚地了解这个模块的依赖性。它们还有非常不同的失败特征——如果没有"datetime"模块,第一个在加载时会失败,而第二个在调用方法之前不会失败。
补充说明:在Ironpython中,导入可能比在cpython中要贵很多,因为代码基本上是在导入时编译的。
我不担心预先加载模块的效率太高。模块占用的内存不会很大(假设它的模块化程度足够),启动成本可以忽略不计。
在大多数情况下,您希望在源文件的顶部加载模块。对于阅读代码的人来说,它可以更容易地分辨哪些函数或对象来自哪个模块。
在代码的其他地方导入模块的一个很好的原因是,如果在调试语句中使用了模块。
例如:
1 | do_something_with_x(x) |
我可以用以下代码调试:
1 2 3 | from pprint import pprint pprint(x) do_something_with_x(x) |
当然,在代码的其他地方导入模块的另一个原因是,如果需要动态导入模块的话。这是因为你几乎没有任何选择。
我不担心预先加载模块的效率太高。模块占用的内存不会很大(假设它的模块化程度足够),启动成本可以忽略不计。
curt提出了一个很好的观点:第二个版本更清晰,在加载时会失败,而不是稍后,而且是意外的。
通常我不担心加载模块的效率,因为它(a)非常快,(b)大部分只在启动时发生。
如果必须在意外的时间加载重量级模块,那么使用
这是一种权衡,只有程序员才能决定做。
案例1通过在需要之前不导入datetime模块(并执行可能需要的任何初始化)来节省一些内存和启动时间。请注意,"仅在被调用时"执行导入也意味着"每次被调用时"执行导入,因此第一次调用之后的每个调用仍然会产生执行导入的额外开销。
案例2通过预先导入datetime来节省一些执行时间和延迟,以便不经常调用的()在调用时返回得更快,并且不会在每次调用时产生导入的开销。
除了效率,如果导入语句是……在前面。将它们隐藏在代码中会使您更难轻松地找到某些东西所依赖的模块。
就我个人而言,我通常遵循PEP,除了单元测试之类的东西,我不希望总是加载它们,因为我知道它们除了测试代码之外不会被使用。
这里有一个例子,所有的进口都在最上面(这是我唯一需要做的时间)。我希望能够在un*x和windows上终止子进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 |
(评论:约翰·米利金所说的。)
这和许多其他优化一样——您牺牲了一些可读性来提高速度。正如约翰提到的,如果你已经完成了你的分析作业,发现这是一个非常有用的足够改变,你需要额外的速度,然后去做。最好把所有其他进口商品都记下来:
1 2 3 | from foo import bar from baz import qux # Note: datetime is imported in SomeClass below |
只需完成moe的答案和原始问题:
当我们必须处理循环依赖时,我们可以做一些"技巧"。假设我们使用的是分别包含
所以,总结一下。如果您没有处理循环依赖关系,并采取某种技巧来避免它们,那么最好将所有导入放在最前面,因为在这个问题的其他答案中已经解释了原因。而且,在做这个"把戏"的时候,包括一个评论,它总是受欢迎的!:)
除了已经给出的很好的答案外,值得注意的是,进口商品的放置不仅仅是一个风格问题。有时模块具有需要先导入或初始化的隐式依赖项,顶级导入可能导致违反所需的执行顺序。
这个问题经常出现在ApacheSark的pythonAPI中,在导入任何pyspark包或模块之前,需要初始化sparkContext。最好将pyspark导入放在保证sparkContext可用的范围内。
模块初始化只在第一次导入时发生一次。如果所讨论的模块来自标准库,那么您也可能从程序中的其他模块导入它。对于像datetime这样流行的模块,它也可能是对一系列其他标准库的依赖。因为模块初始化已经发生了,所以import语句的开销很小。此时正在做的就是将现有模块对象绑定到本地作用域。
将这些信息与可读性的参数结合起来,我认为最好在模块范围内使用import语句。
我不想提供完整的答案,因为其他人已经做得很好了。当我发现在函数内部导入模块特别有用时,我只想提一个用例。我的应用程序使用存储在特定位置的python包和模块作为插件。在应用程序启动期间,应用程序遍历位置中的所有模块并导入它们,然后查看模块内部,如果找到插件的一些安装点(在我的例子中,它是具有唯一ID的某个基类的子类),则注册它们。插件的数量很大(现在有几十个,但将来可能有几百个),而且每个插件很少使用。在插件模块顶部导入第三方库在应用程序启动过程中会受到一些惩罚。尤其是一些第三方的库很难导入(例如,Plotly的导入甚至尝试连接到Internet并下载一些东西,这会给启动增加一秒钟的时间)。通过优化插件中的导入(仅在使用它们的函数中调用它们),我设法将启动时间从10秒缩短到了大约2秒。这对我的用户来说是一个很大的不同。
所以我的答案是不,不要总是把导入放在模块的顶部。
有趣的是,到目前为止,还没有一个答案提到并行处理,在这种情况下,可能需要在函数中导入数据,而序列化函数代码正被推送到其他核心,例如ipyparallel。
通过在函数内部导入变量/局部作用域,可以提高性能。这取决于函数中导入的对象的用法。如果要循环多次并访问模块全局对象,则将其作为本地对象导入会有所帮助。
Py1 2 3 4 5 | X=10 Y=11 Z=12 def add(i): i = i + 10 |
RunLoal.Py
1 2 3 4 5 6 7 8 9 10 11 12 | 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() |
Py
1 2 3 4 5 6 7 8 | from test import add, X, Y, Z def callme(): for i in range(100000000): add(i) X+Y+Z callme() |
在Linux上的一段时间显示出小的收益
1 2 3 4 | /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名称解析
我很惊讶没有看到重复的负载检查的实际成本数字已经张贴,虽然有很多好的解释,对什么期待。
如果在顶部导入,则无论发生什么,都会承受负载命中。这很小,但通常以毫秒为单位,而不是纳秒。
如果在一个函数中导入,那么只有在首次调用其中一个函数时才进行加载。正如许多人指出的那样,如果根本不发生这种情况,就可以节省加载时间。但是,如果函数被多次调用,则需要进行一次重复的、但要小得多的命中(检查它是否已被加载;而不是实际重新加载)。另一方面,正如@aaronasterling指出的那样,您还可以节省一点,因为在函数中导入可以使函数使用更快的局部变量查找来稍后标识名称(http://stackoverflow.com/questions/477096/python import coding style/4789963 4789963)。
下面是从函数内部导入一些东西的简单测试的结果。下面显示了报告的时间(在2.3 GHz Intel Core i7上的python 2.7.14中)(第二次调用比以后调用更频繁,但我不知道为什么)。
1 2 3 4 5 6 7 8 9 10 | 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 |
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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 |
我想提一个我的用例,非常类似于@john millikin和@v.k所提到的用例:
可选导入我使用Jupyter笔记本进行数据分析,并使用相同的ipython笔记本作为所有分析的模板。在某些情况下,我需要导入TensorFlow来执行一些快速的模型运行,但有时我在TensorFlow未设置/导入缓慢的地方工作。在这些情况下,我将依赖于TensorFlow的操作封装在一个助手函数中,在该函数中导入TensorFlow,并将其绑定到一个按钮。
这样,我可以在不等待导入的情况下执行"重新启动并运行所有"操作,或者在导入失败时恢复其余的单元。