导入语句是否应该始终位于模块的顶部?
- 2024-12-09 08:30:00
- admin 原创
- 147
问题描述:
PEP 8规定:
导入总是放在文件的顶部,紧接着任何模块注释和文档字符串,以及模块全局变量和常量之前。
但是,如果我导入的类/方法/函数仅在极少数情况下使用,那么在需要时进行导入肯定会更有效率?
这不是:
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()
解决方案 1:
模块导入速度很快,但不是即时的。这意味着:
将导入放在模块的顶部是可以的,因为这只是一次支付的微不足道的成本。
将导入放在函数内将导致该函数的调用花费更长时间。
因此,如果您关心效率,请将导入放在顶部。仅当您的分析表明这样做有帮助时才将它们移入函数中(您进行了分析以了解哪里可以最好地提高性能,对吗?)
我见过的执行延迟导入的最佳理由是:
可选库支持。如果您的代码有多个使用不同库的路径,则即使未安装可选库,也不会中断。
在插件中
__init__.py
,可能被导入但实际上并未使用。例如,Bazaar 插件使用 的bzrlib
延迟加载框架。
解决方案 2:
将 import 语句放在函数内部可以防止循环依赖。例如,如果您有 2 个模块,X.py 和 Y.py,并且它们都需要相互导入,那么当您导入其中一个模块时,这将导致循环依赖,从而导致无限循环。如果您将 import 语句移动到其中一个模块中,那么在调用该函数之前,它不会尝试导入另一个模块,并且该模块已被导入,因此不会出现无限循环。点击此处了解更多信息 - effbot.org/zone/import-confusion.htm
解决方案 3:
我采取的做法是将所有导入都放在使用它们的函数中,而不是放在模块的顶部。
我获得的好处是能够更可靠地进行重构。当我将函数从一个模块移动到另一个模块时,我知道该函数将继续工作,并且其所有遗留的测试都完好无损。如果我将导入放在模块顶部,那么当我移动函数时,我会发现我最终会花费大量时间来使新模块的导入完整且最小化。重构 IDE 可能会使这变得无关紧要。
其他地方提到过,速度会有所下降。我在我的应用程序中对此进行了测量,发现这对我的目的来说微不足道。
能够预先看到所有模块依赖关系而无需借助搜索(例如 grep)也很不错。但是,我关心模块依赖关系的原因通常是因为我正在安装、重构或移动包含多个文件的整个系统,而不仅仅是单个模块。在这种情况下,我无论如何都要执行全局搜索,以确保我拥有系统级依赖关系。所以我没有找到全局导入来帮助我在实践中理解系统。
我通常将导入sys
放在检查中if __name__=='__main__'
,然后将参数(如sys.argv[1:]
)传递给函数。这允许我在尚未导入的上下文中main()
使用。main
`sys`
解决方案 4:
大多数情况下,这样做会更清晰,也更明智,但情况并非总是如此。下面是一些模块导入可能存在于其他地方的情况示例。
首先,你可以有一个带有以下形式的单元测试的模块:
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()
[...]
可能还存在其他情况,您可能会将导入放置在代码的其他部分。
解决方案 5:
以下是对此问题及其相关问题的答案的
最新摘要
。
PEP 8
建议将导入放在顶部。ImportError
在第一次运行程序时获取s通常
比在程序第一次调用函数时获取 s 更方便
。
将导入放在函数范围内可以帮助避免循环导入的问题。
将导入放在函数范围内有助于保持模块命名空间的干净,以便它不会出现在制表符补全建议中。
启动时间:函数中的导入不会运行,直到(如果)该函数被调用。对于重量级库来说,可能会变得很重要。
尽管导入语句在后续运行中速度非常快,但它们仍然会造成速度损失
,如果该功能很简单但经常使用,这种损失可能会很严重。__name__ == "__main__"
受到监管的进口似乎非常合理。如果导入位于
使用它们的函数中(便于将其移动到另一个模块),重构可能会更容易。也可以说这对可读性有好处。然而,大多数人会持相反观点,请参阅下一项。顶部的导入增强了可读性,因为您可以一眼看到所有的依赖关系。
目前似乎不清楚动态(可能是有条件的)导入是否更倾向于一种风格而不是另一种风格。
解决方案 6:
当函数被调用零次或一次时,第一种变体确实比第二种变体更有效。然而,对于第二次及后续调用,“每次调用都导入”方法实际上效率较低。请参阅此链接,了解一种通过“延迟导入”结合两种方法优点的延迟加载技术。
但是除了效率之外,还有其他原因让您更喜欢其中一种。一种方法是让阅读代码的人更清楚地了解该模块的依赖关系。它们还具有非常不同的故障特征——如果没有“datetime”模块,第一个将在加载时失败,而第二个在调用该方法之前不会失败。
补充说明:在 IronPython 中,导入可能比在 CPython 中昂贵得多,因为代码基本上是在导入时进行编译的。
解决方案 7:
Curt 提出了一个很好的观点:第二个版本更清晰,并且会在加载时失败,而不是稍后失败,并且会出乎意料。
通常我不担心加载模块的效率,因为它 (a) 非常快,并且 (b) 大多只在启动时发生。
如果您必须在意外的时间加载重量级模块,那么使用该__import__
函数动态加载它们可能更有意义,并确保捕获ImportError
异常并以合理的方式处理它们。
解决方案 8:
尽管对于预期结果有很多很好的解释,但我很惊讶没有看到已发布的重复负载检查的实际成本数字。
如果您在顶部导入,无论如何您都会承受负载。这个时间非常小,但通常以毫秒为单位,而不是纳秒。
如果您在函数内导入,则只有在首次调用其中一个函数时才会加载。正如许多人指出的那样,如果根本没有发生这种情况,则可以节省加载时间。但是,如果函数被多次调用,则会重复加载,但加载时间要小得多(用于检查是否已加载;而不是实际重新加载)。另一方面,正如@aaronasterling 指出的那样,您也可以节省一点时间,因为在函数内导入允许函数使用稍快的局部变量查找来稍后识别名称(http://stackoverflow.com/questions/477096/python-import-coding-style/4789963#4789963)。
以下是从函数内部导入一些内容的简单测试的结果。报告的时间(在 2.3 GHz Intel Core 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 xC2xB5s" % (i, (t1-t0)*1E6))
t0 = t1
for i in xrange(5):
bar()
t1 = time()
print(" %2d bar: %12.4f xC2xB5s" % (i, (t1-t0)*1E6))
t0 = t1
解决方案 9:
我不会太担心预先加载模块的效率。模块占用的内存不会很大(假设它足够模块化),启动成本可以忽略不计。
在大多数情况下,您希望在源文件顶部加载模块。对于阅读代码的人来说,这可以更容易地分辨出哪个函数或对象来自哪个模块。
在代码的其他地方导入模块的一个很好的理由是它在调试语句中使用。
例如:
do_something_with_x(x)
我可以使用以下方法调试它:
from pprint import pprint
pprint(x)
do_something_with_x(x)
当然,在代码的其他地方导入模块的另一个原因是如果你需要动态导入它们。这是因为你几乎没有任何选择。
我不会太担心预先加载模块的效率。模块占用的内存不会很大(假设它足够模块化),启动成本可以忽略不计。
解决方案 10:
这是一个权衡,只有程序员才能决定。
案例 1 通过在需要之前不导入 datetime 模块(并执行可能需要的任何初始化)来节省一些内存和启动时间。请注意,“仅在调用时”执行导入也意味着“每次调用时”执行导入,因此第一次调用之后的每次调用仍会产生执行导入的额外开销。
案例 2 通过预先导入 datetime 来节省一些执行时间和延迟,以便 not_often_called() 在调用时能够更快地返回,并且不会在每次调用时产生导入的开销。
除了效率之外,如果导入语句放在前面,那么更容易预先看到模块依赖关系。将它们隐藏在代码中可能会使查找依赖模块变得更加困难。
就我个人而言,我通常会遵循 PEP,但单元测试等我不希望总是加载的东西除外,因为我知道除了测试代码之外不会使用它们。
解决方案 11:
下面是一个示例,其中所有导入都位于最顶部(这是我唯一一次需要这样做)。我希望能够在 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
(回顾:约翰·米利金说过的话。)
解决方案 12:
这就像许多其他优化一样——为了速度牺牲了一些可读性。正如约翰提到的,如果你已经完成了分析工作,发现这是一个非常有用的改变,而且你需要额外的速度,那么就去做吧。最好在所有其他导入上都加一个注释:
from foo import bar
from baz import qux
# Note: datetime is imported in SomeClass below
解决方案 13:
模块初始化只发生一次 - 在第一次导入时。如果所讨论的模块来自标准库,那么您很可能也会从程序中的其他模块导入它。对于像 datetime 这样流行的模块,它也可能是大量其他标准库的依赖项。由于模块初始化已经发生,因此导入语句的成本非常低。此时它所做的只是将现有模块对象绑定到本地范围。
将这些信息与可读性参数结合起来,我认为最好将导入语句放在模块范围内。
解决方案 14:
只是为了完成Moe 的回答和原始问题:
当我们必须处理循环依赖时,我们可以做一些“技巧”。假设我们正在使用分别包含和 b 的模块a.py
和。然后:b.py
`x()`y()
我们可以将其中一个移动
from imports
到模块的底部。我们可以将其中一个移动到
from imports
实际需要导入的函数或方法内部(这并不总是可行的,因为您可能从多个地方使用它)。我们可以将其中之一更改
from imports
为如下导入:import a
所以,总结一下。如果你没有处理循环依赖关系并且使用某种技巧来避免它们,那么最好将所有导入放在顶部,因为原因已在这个问题的其他答案中解释过。并且,请在执行此“技巧”时包含评论,我们总是欢迎的!:)
解决方案 15:
除了已经给出的优秀答案之外,值得注意的是,导入的位置不仅仅是风格问题。有时模块具有需要首先导入或初始化的隐式依赖项,而顶级导入可能会导致违反所需的执行顺序。
此问题经常出现在 Apache Spark 的 Python API 中,您需要在导入任何 pyspark 包或模块之前初始化 SparkContext。最好将 pyspark 导入放在可以保证 SparkContext 可用的范围内。
解决方案 16:
我并不想提供完整的答案,因为其他人已经做得很好了。我只想提一个用例,我发现在函数内导入模块特别有用。我的应用程序使用存储在特定位置的 python 包和模块作为插件。在应用程序启动期间,应用程序遍历该位置中的所有模块并导入它们,然后查看模块内部,如果它找到插件的一些安装点(在我的情况下,它是具有唯一 ID 的某个基类的子类),它会注册它们。插件的数量很大(现在有几十个,但将来可能会有几百个),而且每个插件都很少使用。在我的插件模块顶部导入第三方库在应用程序启动时有点麻烦。特别是一些第三方库导入起来很麻烦(例如,导入 plotly 甚至会尝试连接到互联网并下载一些东西,这会给启动增加大约一秒钟的时间)。通过优化插件中的导入(仅在使用它们的函数中调用它们),我设法将启动时间从 10 秒缩短到 2 秒左右。对于我的用户来说,这是一个很大的不同。
所以我的答案是否定的,不要总是将导入放在模块的顶部。
解决方案 17:
有趣的是,到目前为止还没有一个答案提到并行处理,当序列化函数代码被推送到其他核心时,可能需要将导入放在函数中,例如在 ipyparallel 的情况下。
解决方案 18:
在函数内部导入变量/局部作用域可以提高性能。这取决于函数内部导入内容的用法。如果您循环多次并访问模块全局对象,则将其导入为本地会有所帮助。
测试.py
X=10
Y=11
Z=12
def add(i):
i = i + 10
运行本地
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
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 " %E real, %U user, %S sys" python run.py
0:17.80 real, 17.77 user, 0.01 sys
/tmp/test$ /usr/bin/time -f " %E real, %U user, %S sys" python runlocal.py
0:14.23 real, 14.22 user, 0.01 sys
real 是挂钟。user 是程序中的时间。sys 是系统调用的时间。
https://docs.python.org/3.5/reference/executionmodel.html#resolution-of-names
解决方案 19:
可读性
除了启动性能之外,本地化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
文件顶部会引入不必要的启动滞后。
解决方案 20:
我想提一下我的一个用例,与@John Millikin 和@VK 提到的非常相似:
可选导入
我使用 Jupyter Notebook 进行数据分析,并使用相同的 IPython 笔记本作为所有分析的模板。在某些情况下,我需要导入 Tensorflow 来快速运行一些模型,但有时我在未设置 Tensorflow/导入速度很慢的地方工作。在这些情况下,我将依赖 Tensorflow 的操作封装在一个辅助函数中,在该函数内导入 Tensorflow,并将其绑定到一个按钮。
这样,我就可以执行“重新启动并运行全部”,而不必等待导入,或者在出现故障时恢复其余单元。
解决方案 21:
虽然PEP鼓励在模块顶部导入,但在其他级别导入也不算错误。这表明导入应该在顶部,但也有例外。
在使用模块时加载模块是一种微优化。如果代码导入缓慢,并且效果显著,则可以稍后进行优化。
不过,您可以引入标志来尽可能靠近顶部进行条件导入,从而允许用户使用配置来导入他们需要的模块,同时仍然立即导入所有内容。
尽快导入意味着如果缺少任何导入(或导入的导入)或存在语法错误,程序将失败。如果所有导入都发生在所有模块的顶部,那么 python 会分两步运行。编译。运行。
内置模块设计精良,因此无论在哪里导入,都可以正常工作。您编写的模块应该相同。将导入移至顶部或首次使用处,有助于确保不会产生副作用,并且代码会注入依赖项。
无论您是否将导入放在顶部,当导入位于顶部时,您的代码仍应可以正常工作。因此,请立即开始导入,然后根据需要进行优化。
解决方案 22:
这是一个非常有趣的讨论。像许多其他人一样,我从未考虑过这个话题。由于想在我的某个库中使用 Django ORM,我被逼到了必须在函数中进行导入的地步。我必须django.setup()
在导入我的模型类之前调用它,并且由于它位于文件顶部,因此由于 IoC 注入器构造,它被拖入完全非 Django 库代码中。
我稍微修改了一下,最终将其放入django.setup()
单例构造函数中,并将相关导入放在每个类方法的顶部。现在这工作正常,但让我感到不安,因为导入不在顶部,而且我开始担心导入会额外耗费时间。然后我来到这里,饶有兴趣地阅读了大家对此的看法。
我有很长的 C++ 背景,现在使用 Python/Cython。我对此的看法是,为什么不把导入放在函数中,除非它会导致瓶颈。这就像在需要变量之前声明变量的空间一样。问题是我有数千行代码,所有导入都在顶部!所以我想我会从现在开始这样做,当我经过并有时间时,在这里和那里更改奇怪的文件。