Python导入编码风格

时间:2009-01-25 03:06:10

标签: python coding-style

我发现了一种新模式。这种模式是众所周知的还是对此有什么看法?

基本上,我很难刷新源文件以找出可用的模块导入等等,所以现在,而不是

import foo
from bar.baz import quux

def myFunction():
    foo.this.that(quux)

我将所有导入移动到实际使用的函数中,如下所示:

def myFunction():
    import foo
    from bar.baz import quux

    foo.this.that(quux)

这做了一些事情。首先,我很少意外地用其他模块的内容污染我的模块。我可以为模块设置__all__变量,但随后我必须在模块发展时更新它,这对于实际存在于模块中的代码无法帮助实现命名空间。

其次,我很少在我的模块顶部进行一连串的进口,其中一半或更多我不再需要,因为我已经重构了它。最后,我发现这个模式更容易阅读,因为每个引用的名称都在函数体中。

11 个答案:

答案 0 :(得分:105)

这个问题的(先前)top-voted answer格式很好,但性能绝对错误。让我演示

性能

热门导入

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())


for i in xrange(1000):
    f()

$ time python import.py

real        0m0.721s
user        0m0.412s
sys         0m0.020s

在函数体中导入

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(1000):
    f()

$ time python import2.py

real        0m0.661s
user        0m0.404s
sys         0m0.008s

如您所见,在函数中导入模块可以更多高效。这样做的原因是简单的。它将引用从全局引用移动到本地引用。这意味着,至少对于CPython,编译器将发出LOAD_FAST指令而不是LOAD_GLOBAL指令。顾名思义,这些更快。另一个回答者通过导入循环的每一次迭代来人为地夸大了查看sys.modules的性能影响。

作为一项规则,最好在顶部导入,但如果您多次访问模块,性能。原因是人们可以更容易地跟踪模块所依赖的内容,并且这样做与Python世界的大部分内容一致。

答案 1 :(得分:53)

这确实有一些缺点。

测试

如果您希望通过运行时修改来测试模块,可能会使其变得更加困难。而不是做

import mymodule
mymodule.othermodule = module_stub

你必须要做

import othermodule
othermodule.foo = foo_stub

这意味着您必须全局修补othermodule,而不是仅仅更改mymodule中的引用指向的内容。

依赖关系跟踪

这使得模块所依赖的模块不明显。如果您使用许多第三方库或重新组织代码,这尤其令人恼火。

我必须维护一些遗留代码,这些代码在整个地方使用了内联导入,这使得代码极难重构或重新打包。

绩效说明

由于python缓存模块的方式,没有性能损失。实际上,由于模块位于本地名称空间中,因此在函数中导入模块会有一些性能上的好处。

热门导入

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()


$ time python test.py 

real   0m1.569s
user   0m1.560s
sys    0m0.010s

在函数体中导入

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()

$ time python test2.py

real    0m1.385s
user    0m1.380s
sys     0m0.000s

答案 2 :(得分:21)

这种方法存在一些问题:

  • 打开文件所依赖的模块时,并不是很明显。
  • 它会混淆必须分析依赖关系的程序,例如py2exepy2app等。
  • 您在许多功能中使用的模块怎么样?你最终会得到很多冗余的导入,或者你必须在文件的顶部和一些内部函数中有一些。

所以...首选的方法是将所有导入放在文件的顶部。我发现如果我的导入很难跟踪,通常意味着我有太多的代码,我最好把它分成两个或更多的文件。

已经发现函数内部导入有用的一些情况:

  • 处理循环依赖(如果你真的无法避免它们)
  • 平台特定代码

另外:在每个函数中放入导入实际上比文件顶部慢得多。第一次加载每个模块时,它被放入sys.modules,每次后续导入只花费查找模块的时间,这相当快(不会重新加载)。

答案 3 :(得分:10)

需要注意的另一个有用的事情是在Python 3.0中删除了函数内部的from module import *语法。

在此处的“删除的语法”中简要提及了它:

http://docs.python.org/3.0/whatsnew/3.0.html

答案 4 :(得分:4)

我建议你尽量避免导入from foo import bar。我只在包中使用它们,其中拆分模块是一个实现细节,无论如何都不会有很多。

在导入包的所有其他位置,只需使用import foo,然后使用全名foo.bar引用它。这样,您始终可以告诉某个元素来自何处,而不必维护导入元素的列表(实际上,这将永远过时并导入不再使用的元素)。

如果foo是一个非常长的名称,您可以使用import foo as f对其进行简化,然后编写f.bar。这比维护所有from导入更加方便和明确。

答案 5 :(得分:3)

人们已经很好地解释了为什么要避免内联导入,而不是真正的替代工作流来解决你想要的原因。

  

我很难刷新源文件以找出可用的模块导入等等

要检查未使用的导入,请使用pylint。它执行静态(ish) - Python代码分析,它检查的(很多)事情之一是未使用的导入。例如,以下脚本..

import urllib
import urllib2

urllib.urlopen("http://stackoverflow.com")

..会生成以下消息:

example.py:2 [W0611] Unused import urllib2

至于检查可用的导入,我通常依赖TextMate(相当简单)完成 - 当你按Esc时,它会在文档中与其他人一起完成当前单词。如果我已完成import urlliburll[Esc]将扩展为urllib,否则我会跳转到文件的开头并添加导入。

答案 6 :(得分:2)

从性能的角度来看,您可以看到:Should Python import statements always be at the top of a module?

一般情况下,我只使用本地导入来破坏依赖循环。

答案 7 :(得分:2)

我认为在某些情况/场景中这是推荐的方法。例如,在Google App Engine中建议使用延迟加载大模块,因为它可以最大限度地降低实例化新Python VM /解释器的预热成本。看一下描述这个的Google Engineer's演示文稿。但请记住,意味着您应该延迟加载所有模块。

答案 8 :(得分:2)

您可能需要查看python wiki中的Import statement overhead。简而言之:如果模块已经加载(查看sys.modules),您的代码将运行得更慢。如果您的模块尚未加载,foo只会在需要时加载,这可能是零次,那么整体性能会更好。

答案 9 :(得分:0)

两种变体都有其用途。但是在大多数情况下,最好在函数之外导入,而不是在函数内部导入。

效果

在几个答案中已经提到过,但在我看来,他们都缺乏完整的讨论。

第一次在python解释器中导入模块时,无论它是在顶层还是在函数内部,它都会很慢。它很慢,因为Python(我专注于CPython,它可能与其他Python实现不同)做了多个步骤:

  • 找到包裹。
  • 检查包是否已经转换为字节码(着名的__pycache__目录或.pyx文件),如果没有,则将它们转换为字节码。
  • Python加载字节码。
  • 已加载的模块位于sys.modules

后续导入不必完成所有这些操作,因为Python只能从sys.modules返回模块。所以后续进口会快得多。

可能是您模块中的某个功能实际上并未经常使用,但它取决于花费很长时间的import。然后你可以实际移动函数内的import。这将使您的模块更快地导入(因为它不必立即导入长加载包)但是当最终使用该函数时,它在第一次调用时会很慢(因为那时必须导入模块) 。这可能会对感知到的性能产生影响,因为不会减慢所有用户的速度,而只会减慢那些使用依赖于缓慢加载依赖性的函数的速度。

然而sys.modules中的查询并不是免费的。它速度非常快,但并不是免费的。因此,如果你实际上经常调用import一个包的函数,你会注意到性能略有下降:

import random
import itertools

def func_1():
    return random.random()

def func_2():
    import random
    return random.random()

def loopy(func, repeats):
    for _ in itertools.repeat(None, repeats):
        func()

%timeit loopy(func_1, 10000)
# 1.14 ms ± 20.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit loopy(func_2, 10000)
# 2.21 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

这差不多两倍。

认识aaronasterling "cheated" a bit in the answer非常重要。他表示,在函数中进行导入实际上会使函数更快。在某种程度上,这是事实。这是因为Python如何查找名称:

  • 首先检查本地范围。
  • 接下来检查周围的范围。
  • 然后检查下一个周围范围
  • ...
  • 检查全局范围。

因此,不是检查本地范围然后检查全局范围,而是检查本地范围,因为模块的名称在本地范围内可用。这实际上使它更快!但这是一种名为"Loop-invariant code motion"的技术。它基本上意味着通过在循环(或重复调用)之前将其存储在变量中来减少循环(或重复)中完成的事情的开销。因此,您可以简单地使用变量并将其分配给全局名称,而不是import在函数中使用它:

import random
import itertools

def f1(repeats):
    "Repeated global lookup"
    for _ in itertools.repeat(None, repeats):
        random.random()

def f2(repeats):
    "Import once then repeated local lookup"
    import random
    for _ in itertools.repeat(None, repeats):
        random.random()

def f3(repeats):
    "Assign once then repeated local lookup"
    local_random = random
    for _ in itertools.repeat(None, repeats):
        local_random.random()

%timeit f1(10000)
# 588 µs ± 3.92 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f2(10000)
# 522 µs ± 1.95 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f3(10000)
# 527 µs ± 4.51 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

虽然您可以清楚地看到对全局random执行重复查找的速度很慢,但在函数内部导入模块或在函数内部的变量中分配全局模块之间几乎没有区别。 / p>

这也可以通过避免循环中的函数查找来达到极限:

def f4(repeats):
    from random import random
    for _ in itertools.repeat(None, repeats):
        random()

def f5(repeats):
    r = random.random
    for _ in itertools.repeat(None, repeats):
        r()

%timeit f4(10000)
# 364 µs ± 9.34 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f5(10000)
# 357 µs ± 2.73 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

再次快得多,但导入和变量几乎没有区别。

可选依赖项

有时进行模块级导入实际上可能是个问题。例如,如果您不想添加其他安装时依赖项,但该模块对某些其他功能非常有帮助。决定依赖项是否应该是可选的不应该轻易完成,因为它会影响用户(如果他们遇到意外的ImportError或者错过了很酷的功能")并且它使所有功能更复杂的安装包,正常的依赖pipconda(只提两个包管理器)开箱即用,但对于可选的依赖,用户必须稍后手动安装包-on(有一些选项可以自定义要求,但是再次安装它的负担"正确"放在用户身上)。

但这又可以通过两种方式完成:

try:
    import matplotlib.pyplot as plt
except ImportError:
    pass

def function_that_requires_matplotlib():
    plt.plot()

或:

def function_that_requires_matplotlib():
    import matplotlib.pyplot as plt
    plt.plot()

通过提供替代实现或自定义用户看到的异常(或消息),可以更加自定义,但这是主要要点。

如果想要提供另一种解决方案,那么顶级方法可能会更好一些"对于可选的依赖项,但通常人们使用函数内导入。主要是因为它导致更清晰的堆栈跟踪并且更短。

循环进口

函数导入对于避免由于循环导入导致的ImportErrors非常有用。在很多情况下,循环进口是“坏”的标志。包结构,但如果绝对没有办法避免循环导入" circle" (以及问题)通过将导致圆圈的导入放入实际使用它的函数中来解决。

不要重复自己

如果您实际将所有导入放入函数而不是模块范围,则会引入冗余,因为函数可能需要相同的导入。这有一些缺点:

  • 您现在有多个地方可以检查导入是否已过时。
  • 如果您错误地导入某些导入,您只会在运行特定功能时发现,而不是在加载时发现。因为你有更多的import语句错误的可能性增加(不多),它只是变得更加重要,以测试所有函数。

其他想法:

  

我很少在我的模块顶部进行一连串的进口,其中一半或更多我不再需要,因为我已经重构了它。

大多数IDE已经有未使用导入的检查程序,因此只需点击几下即可删除它们。即使您没有使用IDE,也可以偶尔使用静态代码检查器脚本并手动修复它。另一个答案提到了pylint,但还有其他答案(例如pyflakes)。

  

我很少意外地用其他模块的内容污染我的模块

这就是为什么您通常使用__all__和/或定义函数子模块并仅导入主模块中的相关类/函数/ ...的原因,例如__init__.py

另外,如果您认为自己污染了模块命名空间太多,那么您可能应该考虑将模块拆分为子模块,但这只适用于几十个导入。

如果要减少命名空间污染,另外一个(非常重要的)要点是避免from module import *导入。但是,您可能还希望避免导入太多名称的from module import a, b, c, d, e, ...导入,只需导入模块并使用module.c访问这些功能。

作为最后的手段,您始终可以使用别名来避免使用" public"来污染命名空间。使用:import random as _random导入。这将使代码更难理解,但它非常清楚应该公开显示什么,不应该是什么。这不是我建议的,你应该让__all__列表保持最新(这是推荐的,明智的方法)。

摘要

  • 性能影响是显而易见的,但几乎总是会进行微观优化,所以不要让微软基准指导你做出口的决定。除非第一个import上的依赖关系真的很慢,并且它仅用于该功能的一小部分。然后,对于大多数用户来说,它实际上可以对模块的感知性能产生明显的影响。

  • 使用常用的工具来定义公共API,我的意思是__all__变量。保持最新状态可能有点烦人,但是检查过时导入的所有功能或添加新功能以添加该功能中的所有相关导入也是如此。从长远来看,您可能需要通过更新__all__来减少工作量。

  • 你喜欢哪一个并不重要,两者都有效。如果你独自工作,你可以推断出利弊,做一个你认为最好的做法。但是,如果你在一个团队中工作,你可能应该坚持使用已知模式(这将是__all__的顶级导入),因为它允许他们做(他们可能)总是做的事情。

    < / LI>

答案 10 :(得分:-1)

安全实施

考虑一个环境,其中所有Python代码都位于只有特权用户才能访问的文件夹中。为了避免以特权用户身份运行整个程序,您决定在执行期间将权限删除给非特权用户。一旦您使用导入另一个模块的函数,您的程序将抛出ImportError,因为由于文件权限,非特权用户无法导入模块。