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

Ada*_*ter 373 python optimization coding-style

PEP 08指出:

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

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

不是这个:

class SomeClass(object):

    def not_often_called(self)
        from datetime import datetime
        self.datetime = datetime.now()
Run Code Online (Sandbox Code Playgroud)

比这更有效率?

from datetime import datetime

class SomeClass(object):

    def not_often_called(self)
        self.datetime = datetime.now()
Run Code Online (Sandbox Code Playgroud)

Joh*_*kin 264

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

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

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


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

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

  • >将导入放在函数中将导致对该函数的调用花费更长时间.实际上,我认为这笔费用只支付一次.我已经读过Python缓存导入的模块,因此再次导入它只需要很少的成本. (37认同)
  • @halfhourhacks Python不会重新导入模块,但它仍然需要执行一些指令,只是为了查看模块是否存在/在sys.modules /等中. (24认同)
  • -1.将导入放在一个函数中并不一定会导致它需要更长的时间.关于另一个问题,请参阅我的[回答](http://stackoverflow.com/questions/477096/python-import-coding-style/4789963#4789963). (23认同)
  • 约翰,这是一个完全理论上的问题,所以我没有任何代码可以分析.在过去,我一直关注PEP,但今天我写了一些代码,让我想知道这是不是正确的事情.谢谢你的帮助. (16认同)
  • 此答案侧重于“import”语句成本,而不是导入 _what_ 的成本。考虑正在导入的库。某些库可能会在导入时运行昂贵的操作(运行时或内存成本)。 (8认同)
  • 一个用例是避免循环导入(通常不合理,但有时候是合适的).有时,模块m1中的类A调用模块m2中的类B上的方法,该方法构造类A的另一个实例.如果构造类A实例的类B中的方法仅在执行构造实例的函数时才执行导入,避免循环导入. (4认同)
  • 我不同意。有时您关心“支付一次”的成本,因为您_每次_启动程序时都需要支付,并且_在_之前_您的代码开始执行任何有用的操作,并且某些常见模块需要花费相当长的时间才能导入。以 numpy、pyplot、cv2、pandas 为例,这些模块中的每个模块需要 0.2 到 1 秒的时间才能导入到我的 PC 上。这使得启动相当缓慢。另一方面,函数内部的导入将不会被注意到,除非您将其放入一些不应该在 python 中编写的关键代码中。重新导入同一个模块1000000次大约需要0.1s。 (2认同)

Moe*_*Moe 76

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

  • 知道这很好,但这是好事还是坏事? (16认同)
  • 当X需要Y并且Y需要X时,它们要么是同一想法的两个部分(即应该一起定义),要么缺少抽象。 (16认同)
  • 面向对象编程经常导致我陷入循环依赖。一个重要的对象类可以导入到多个模块中。为了使该对象执行其自己的任务,它可能需要访问这些模块中的一个或多个。有一些方法可以避免它,例如通过函数args向对象发送数据,以允许它访问其他模块。但有时这样做感觉与 OOP 非常违反直觉(外界不需要知道它如何在该函数中完成任务)。 (5认同)
  • 如果两个模块需要互相导入,则代码有严重问题。 (3认同)
  • 是的,但是一个人可以进入依赖地狱. (2认同)

小智 58

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

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

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

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

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

  • 许多IDE通过优化和自动导入必要的模块到您的文件中来简化代码的重构.在大多数情况下,PyCharm和Eclipse为我做出了正确的决定.我敢打赌,有一种方法可以在emacs或vim中获得相同的行为. (4认同)
  • 这让我感到非常理由在函数内而不是在全局范围内导入.我很惊讶没有其他人因为同样的原因提到这样做.除了性能和冗长之外,还有任何重大缺点吗? (4认同)
  • 全局命名空间中if语句的导入仍然是全局导入.这将打印参数(使用Python 3):`def main():print(sys.argv); 如果为True:import sys; main();`你必须在一个函数中包装`if __name __ =='__ main __'`以创建一个新的命名空间. (3认同)
  • 我发现将导入放在靠近我使用它们的位置对于重构非常有用。不再需要多次滚动到顶部和返回。我使用 pycharm 或 wing ide 等 IDE,也使用它们的重构,但我并不总是想依赖它们。使用这种替代导入样式将函数移动到另一个模块变得更加容易,因此我重构了更多。 (2认同)
  • 此建议的一个主要缺点是您可能没有意识到您已经引入了循环依赖项。然后,您将遇到更大的重构问题,甚至需要修复 API 损坏问题。 (2认同)

Con*_*lls 38

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

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

if __name__ == '__main__':
    import foo
    aa = foo.xyz()         # initiate something for the test
Run Code Online (Sandbox Code Playgroud)

其次,您可能需要在运行时有条件地导入一些不同的模块.

if [condition]:
    import foo as plugin_api
else:
    import bar as plugin_api
xx = plugin_api.Plugin()
[...]
Run Code Online (Sandbox Code Playgroud)

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


Pat*_*ick 18

以下是此问题及相关问题的答案的 更新摘要

  • PEP 8 建议将导入放在顶部。
  • 当你第一次运行你的程序时,而不是当你的程序第一次调用你的函数时,获取 ImportErrors通常更方便。
  • 将导入放入函数作用域有助于避免循环导入的问题。
  • 将导入放入函数作用域有助于保持干净的模块名称空间,以便它不会出现在制表符完成建议中。
  • 启动时间:函数中的导入将不会运行,直到(如果)调用该函数。对于重量级库来说可能会变得很重要。
  • 尽管 import 语句在后续运行中速度非常快,但它们仍然会带来速度损失 ,如果函数很简单但经常使用,则速度损失可能会很大。
  • __name__ == "__main__"监管下的进口似乎非常合理
  • 如果导入位于使用它们的 函数中 (有助于将其移动到另一个模块),那么重构可能会更容易。也可以说这有利于可读性。然而,大多数人会持相反的观点,请参阅下一条。
  • 顶部的导入增强了可读性,因为您可以一目了然地看到所有依赖项。
  • 似乎不清楚动态(可能是有条件的)导入是否更喜欢一种风格而不是另一种风格。


Cur*_*her 14

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

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

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


Dan*_*ski 9

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

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

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


Jas*_*ker 8

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

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

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

例如:

do_something_with_x(x)
Run Code Online (Sandbox Code Playgroud)

我可以调试这个:

from pprint import pprint
pprint(x)
do_something_with_x(x)
Run Code Online (Sandbox Code Playgroud)

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

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


pjz*_*pjz 6

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

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

案例2通过导入日期时间提前,这样not_often_called()将返回得更快,当它节省一些执行时间和等待时间调用,也不会招致每次通话的进口开销.

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

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

  • -1.导入的主要开销仅在第一次发生.在`sys.modules`中查找模块的成本很容易被只需查找本地名称而不是全局名称的节省所抵消. (2认同)

gil*_*tay 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
Run Code Online (Sandbox Code Playgroud)

(回顾:John Millikin说的话.)


Dre*_*ens 6

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

from foo import bar
from baz import qux
# Note: datetime is imported in SomeClass below
Run Code Online (Sandbox Code Playgroud)


Jer*_*own 5

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

将这些信息与可读性参数结合起来,我想说最好在模块范围内使用 import 语句。


Cau*_*ons 5

只是为了完成Moe 的回答和原来的问题:

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

  1. from imports我们可以移动模块底部的其中之一。
  2. from imports我们可以移动实际需要导入的函数或方法内部之一(这并不总是可能的,因为您可以从多个地方使用它)。
  3. 我们可以将两者之一更改from imports为如下所示的导入:import a

所以,总结一下。如果您不处理循环依赖项并采取某种技巧来避免它们,那么最好将所有导入放在顶部,因为这个问题的其他答案中已经解释了原因。请在执行此“技巧”时发表评论,我们总是欢迎的!:)


Tex*_*eek 5

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

如果在顶部导入,则无论如何都会承受负载。这非常小,但通常以毫秒为单位,而不是纳秒。

如果导入功能(S)之内,那么你只需要命中的加载,如果首次调用这些功能之一。正如许多人指出的那样,如果根本没有发生这种情况,则可以节省加载时间。但是,如果函数被多次调用,您会重复但较小的命中(用于检查它是否已加载;而不是实际重新加载)。另一方面,正如@aaronasterling 指出的那样,您还可以节省一点,因为在函数内导入可以让函数使用稍微快一点的局部变量查找来稍后识别名称(http://stackoverflow.com/questions/477096/python-导入编码样式/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
Run Code Online (Sandbox Code Playgroud)

编码:

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
Run Code Online (Sandbox Code Playgroud)

  • 运行时间的变化可能是由于 CPU 频率调整以响应负载所致。最好从一秒钟的繁忙工作开始速度测试,以提高 CPU 时钟速度。 (2认同)