Python导入语句应该总是在模块的顶部?
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()
模块导入非常快,但不是即时的。 这意味着:
- 把进口放在模块的顶部是很好的,因为这是一个微不足道的成本,只支付一次。
- 将导入放在一个函数中会导致对该函数的调用需要更长的时间。
所以如果你关心效率,把进口放在最前面。 只有当你的分析显示会有帮助时,才将它们移动到一个函数中(你确实知道怎样才能改善性能,对吗?)
我见过执行懒惰进口的最好的理由是:
- 可选库支持。 如果你的代码有多个使用不同库的路径,如果没有安装可选库,请不要中断。
- 在插件的
__init__.py
中,可能会导入,但实际上并未使用。 例子是Bazaar插件,它使用bzrlib
的延迟加载框架。
将导入语句放入函数中可以防止循环依赖。 例如,如果你有两个模块,X.py和Y.py,它们都需要相互导入,当你导入一个导致无限循环的模块时,这将导致循环依赖。 如果您将import语句移动到其中一个模块中,那么它将不会尝试导入另一个模块,直到该函数被调用,并且该模块已经被导入,所以没有无限循环。 在这里阅读更多 – effbot.org/zone/import-confusion.htm
我采取了将所有进口放入使用它们的函数的做法,而不是放在模块的顶部。
我得到的好处是能够更可靠地进行重构。 当我将一个功能从一个模块移动到另一个模块时,我知道这个功能将继续完整地完成所有的测试工作。 如果我在模块的顶部有我的导入,当我移动一个函数的时候,我发现我最终花费了大量的时间来使得新模块的导入完成和最小化。 重构IDE可能会使这个不相关。
有其他地方提到的速度惩罚。 我在我的申请中测量了这一点,发现这对我的目的来说是微不足道的。
能够看到所有的模块依赖关系也是很好的,而不需要搜索(例如grep)。 然而,我关心模块依赖关系的原因通常是因为我正在安装,重构或移动包含多个文件的整个系统,而不仅仅是一个模块。 在这种情况下,我要执行全局搜索,以确保我具有系统级别的依赖关系。 所以我没有找到全球进口来帮助我理解一个系统的实践。
我通常把进口的sys
内的if __name__=='__main__'
检查,然后传递参数(如sys.argv[1:]
)到一个main()
函数。 这允许我在没有导入sys
的上下文中使用main
。
大多数情况下,这对于清晰和明智的做法是有用的,但情况并非总是如此。 以下是几个模块导入可能存在于别处的情况的例子。
首先,您可以使用以下格式的单元测试:
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() [...]
在其他情况下,您可能会将代码中的其他部分导入。
当函数被调用为零次或一次时,第一个变种确实比第二个变种更有效。 然而,随着第二次和随后的调用,“导入每个调用”方法实际上效率较低。 看到这个链接的延迟加载技术,通过做一个“懒惰的导入”结合了两种方法的最佳。
但是除了效率以外,还有其他的原因,为什么你可能更喜欢一个。 一种方法是让读者更清楚地了解这个模块所具有的依赖关系。 它们也具有非常不同的故障特征 – 如果没有“datetime”模块,则第一个将在加载时失败,而第二个在调用该方法之前不会失败。
添加注意:在IronPython中,导入可能比CPython贵很多,因为代码基本上是在导入时编译的。
Curt提出了一个很好的观点:第二个版本更加清晰,并且在加载时会失败,而不是以后,并且出乎意料。
通常我不担心加载模块的效率,因为它(a)非常快,(b)大多只在启动时发生。
如果你不得不在意外的时候加载重量级模块,那么使用__import__
函数动态加载它们可能会更有意义,并且一定要捕获ImportError
异常,并以合理的方式处理它们。
这是一个权衡,只有程序员才能决定。
情况1通过不导入日期时间模块(并进行任何可能需要的初始化)来保存一些内存和启动时间,直到需要为止。 请注意,只有在被调用的情况下执行导入也意味着每次调用时都这样做,因此在第一个调用之后的每个调用仍然会导致执行导入的额外开销。
情况2通过预先导入日期时间来节省一些执行时间和等待时间,这样not_often_called()在调用时会更快地返回,并且不会导致每次调用时导入的开销。
除了效率之外,如果导入语句在前面,则更容易看到模块依赖关系。 将它们隐藏在代码中可以使得更容易找到依赖的模块更加困难。
就个人而言,我通常遵循PEP,除了单元测试之类的东西,我不想总是加载,因为我知道除了测试代码之外,他们不会被使用。
下面是一个例子,其中所有的输入都在顶端(这是我唯一需要这样做的时间)。 我希望能够在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所说的话)
我不担心太多的加载模块的效率。 模块占用的内存不会很大(假设它足够模块化),启动成本可以忽略不计。
在大多数情况下,您想要在源文件的顶部加载模块。 对于读取你的代码的人来说,告诉哪个函数或对象来自哪个模块要容易得多。
在代码的其他地方导入模块的一个很好的理由是它是否在调试语句中使用。
例如:
do_something_with_x(x0
我可以调试这个:
from pprint import pprint pprint(x) do_something_with_x(x)
当然,在代码的其他地方导入模块的另一个原因是如果你需要动态地导入它们。 这是因为你几乎没有任何选择。
我不担心太多的加载模块的效率。 模块占用的内存不会很大(假设它足够模块化),启动成本可以忽略不计。
这就像许多其他的优化 – 你牺牲了速度的可读性。 正如约翰所说,如果你已经完成了你的分析作业,发现这是一个非常有用的变化,你需要额外的速度,然后去做。 将所有其他进口记录下来可能是件好事:
from foo import bar from baz import qux # Note: datetime is imported in SomeClass below
模块初始化只发生一次 – 在第一次导入。 如果有问题的模块来自标准库,那么您可能也会从程序中的其他模块中导入它。 对于像日期时间这样流行的模块,它也可能是其他标准库的依赖。 进口声明成本非常低,因为模块初始化已经发生了。 现在所做的就是将现有模块对象绑定到本地作用域。
将这些信息与可读性的参数结合起来,我会说在模块范围内最好有import语句。
只是为了完成萌的回答和原来的问题:
当我们必须处理循环依赖时,我们可以做一些“技巧”。 假设我们正在分别使用包含x()
和b y()
模块a.py
和b.py
然后:
- 我们可以在模块的底部移动一个
from imports
。 - 我们可以在实际需要导入的函数或方法内移动其中一个导入(这不总是可能的,因为您可能会在几个地方使用它)。
- 我们可以将其中的一个
from imports
改为导入,如下所示:import a
所以,总结一下。 如果你没有处理循环依赖和避免它们的某种诡计,那么最好把所有的输入放在顶端,因为这个问题的其他答案已经解释了这个原因。 请在做这个“诀窍”时加上评论,这是非常欢迎的! 🙂
除了已经给出的优秀答案之外,值得注意的是进口的放置不仅仅是一种风格问题。 有时候模块有隐含的依赖关系,需要首先导入或初始化,而顶级导入可能会导致违反所需的执行顺序。
这个问题通常出现在Apache Spark的Python API中,您需要在导入任何pyspark包或模块之前初始化SparkContext。 最好将pyspark导入放在SparkContext保证可用的范围内。
我不想提供完整的答案,因为其他人已经做得很好。 我只想提到一个用例,当我发现在函数内部导入模块特别有用时。 我的应用程序使用python包和模块作为插件存储在特定的位置。 在应用程序启动过程中,应用程序遍历位置中的所有模块并导入它们,然后在模块内部查找,如果它找到插件的某些挂载点(在我的情况下,它是某个基类的子类, ID)它注册它们。 插件的数量很大(现在几十个,但将来可能会有几百个),而且每个插件的使用都很少。 在我的插件模块的顶部导入了第三方库,在应用程序启动过程中有一点损失。 特别是一些第三方库很重要(例如,导入甚至试图连接到互联网,并下载一些东西,启动时间大约增加一秒)。 通过在插件中优化导入(只在他们使用的函数中调用它们),我设法将启动从10秒缩短到2秒。 这对我的用户来说是一个很大的不同。
所以我的答案是否定的,不要总是把进口放在你的模块的顶部。