Python导入编码风格

74

我发现了一种新的模式。这个模式是否广为人知,或者对此有什么看法?

基本上,我很难在源文件中上下查找以确定可用的模块导入等信息,因此现在,我不再使用

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__变量,但那样的话我就必须随着模块的演化来更新它,并且这对于实际存在于模块中的代码的命名空间污染没有帮助。

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


1
可能是重复的问题:Python导入语句是否应该总是放在模块顶部? - Sveltely
10个回答

129

(先前的) 得票最高的答案 对于性能方面的描述虽然格式很好,但是完全是错误的。让我来演示一下。

性能

顶级导入

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世界的大部分代码保持一致。


6
引用实际的字节码差异值得一赞......有时如果我要在函数中频繁使用某些类属性,我会将它们变成局部变量(这样可以更干净的生成字节码和代码)。 - lunixbochs
6
具有导入模块会带来相当大的惩罚。在该函数中,局部访问和循环存在掩盖了这一点。如果您在“顶级导入示例”中添加“r = random”,并使用“r.random()”,则将获得与第二个示例相同的性能。如果您添加“r = random.random”,然后使用“r()”,它甚至会更快。 - H Krishnan
1
@HKrishnan,完全正确。我想我没有表达清楚。如果模块被访问很多次,那么在函数中导入将更快。你建议的方法会更快,但只有在调用导入函数多次时才会稍微快一点。总的来说,我支持在模块级别导入。唯一我能想到需要在函数中导入的情况是当预计程序正常执行期间不会调用该函数并且具有独特的导入时。Django视图就是一个很好的例子。 - aaronasterling
4
如果您只是为了性能而想使用LOAD_FAST,请在全局级别导入random,然后在本地范围内设置random_ = random并使用random_(最好使用random_ = random.randomfrom random import random来保存一些属性查找)。仅为了使用LOAD_FAST而在每个函数调用上进行导入的性能损失是一个不好的主意。 - user2357112

57

这种方法有一些缺点。

测试

如果您希望通过运行时修改来测试模块,则可能会更加困难。不要像下面这样做:

import mymodule
mymodule.othermodule = module_stub

你必须要做

import othermodule
othermodule.foo = foo_stub

这意味着你需要在全局范围内打补丁(patch)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

3
您可能需要澄清一下——每次都会检查导入,但模块只会被加载一次。 - S.Lott
1
感谢您的输入。即使模块被缓存,它仍然会对性能产生很大的影响,正如您从我的测试中所看到的那样。 - Ryan
是的,但现在你已经让它清晰明了。之前很容易误导人。我撤销了我的负评。 - nosklo
2
这并不是一个很好的例子,因为你把import放在了for循环内部而不是在f()函数定义内部。但是,通常来说局部导入确实会有一定的代价。 - davidavr
我之前这样做主要是因为懒惰(使用嵌套循环来执行10^6次),而不是使用单个循环和xrange(10**6)。如果我在测试体中使用非嵌套循环并增加计数,性能应该是相似的。 - Ryan
1
@Ryan,将导入语句放在循环内部比放在外部更容易,您的回答关于性能是完全错误的。请参见我的回答 - aaronasterling

24

这种方法存在一些问题:

  • 打开文件时不会立即显而易见它依赖哪些模块。
  • 这会让需要分析依赖关系的程序(如py2exepy2app等)感到困惑。
  • 那使用在多个函数中的模块怎么办?你要么会最终拥有很多冗余的导入,要么就得在文件顶部和某些函数内部分别导入。

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

以下是我发现将导入放在函数内部有用的一些情况:

  • 处理循环依赖(如果您确实无法避免)
  • 平台特定的代码

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


+1:而且它很慢。每个函数调用都必须重复导入模块检查。 - S.Lott

11

另一个值得注意的事情是,在Python 3.0中,函数内部的 from module import * 语法已被删除。

这里有一个简短的提及,位于“Removed Syntax”下面:

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


1
-1:错误。只有“from xxx import *”形式已被禁用于函数。 - nosklo
6
他说一些部件已被禁用。不要轻易给提供有用信息的人点踩。 - Daniel Naab

5
我建议您尽量避免使用 from foo import bar 这样的导入方式。我只在包内部使用它们,其中模块的拆分是一个实现细节,而且不会有很多。
在所有其他导入包的地方,只需使用 import foo,然后通过完整名称 foo.bar 引用它。这样,您始终可以知道某个元素来自哪里,无需维护导入元素的列表(实际上,这将始终过时并且导入不再使用的元素)。
如果 foo 是一个非常长的名称,则可以使用 import foo as f 简化它,然后写入 f.bar。这仍然比维护所有 from 导入更方便和明确。

4

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

性能

已经在几个答案中提到了这一点,但我认为它们都缺乏完整的讨论。

第一次在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 "在答案中有点作弊"。他说在函数中进行导入实际上会使函数更快。在某种程度上,这是正确的。因为Python查找名称的方式如下:

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

因此,不必检查本地范围再检查全局范围,只需检查本地范围即可,因为模块的名称在本地范围内可用。这实际上使其更快!但这是一种称为"循环不变代码移动"的技术。它基本上是指通过在循环(或重复调用)之前将其存储在变量中来减少循环中完成的某些操作的开销。因此,您可以在函数中使用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很慢,但在函数内导入模块或在函数内将全局模块分配给变量几乎没有区别。

这也可以通过避免循环内的函数查找而被推到极致:

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(仅举两个包管理器)可以直接使用,但对于可选的依赖关系,用户必须稍后手动安装软件包(有一些选项可以使其能够自定义要求,但再次将“正确”安装的负担放在了用户身上)。

但是这可以用两种方式来完成:

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()

这可以通过提供替代实现或自定义异常(或消息)来更加定制化,但这是主要内容。
如果想为可选依赖项提供另一种“解决方案”,则顶层方法可能会更好,但通常人们使用函数内导入。大多数情况下,这会导致更清洁的堆栈跟踪并且更短。
循环导入
在函数内导入可以非常有助于避免由于循环导入而导致的 ImportError。在许多情况下,循环导入是“不良”包结构的标志,但如果绝对无法避免循环导入,则将导致循环的导入放置在实际使用它的函数中即可解决问题。
不要重复自己
如果实际上将所有导入放在函数而不是模块范围内,您将引入冗余,因为函数可能需要相同的导入。这有一些缺点:
- 现在您有多个地方要检查是否有任何导入已过时。 - 如果拼写错误了某个导入,您只会在运行特定函数时发现,而不是在加载时间上。由于有更多的导入语句,出现错误的可能性会增加(不多),只是变得微不足道地测试所有功能更加重要。

其他想法:

我很少在模块顶部使用大量导入,其中一半或更多的导入可能已经不再需要,因为我已经重构了它。

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

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

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

此外,如果您认为模块命名空间太过混乱,则应考虑将模块拆分为子模块,但这仅在有数十个导入项时才有意义。

如果您想要减少命名空间污染,一个非常重要的点是避免使用from module import *导入。您也可以避免导入过多名称的from module import a, b, c, d, e, ...导入,并只导入模块并使用module.c访问函数。
作为最后的手段,您总是可以使用别名来避免通过使用import random as _random将"public"导入污染命名空间。这将使代码更难理解,但它清楚地表明什么应该是公开可见的,什么不应该是。我不建议这样做,您应该保持__all__列表最新(这是推荐和明智的方法)。

摘要

  • 性能影响是可见的,但几乎总是微观优化,因此不要让放置导入语句的决定受到微基准测试的指导。除非依赖项在第一个import上真的很慢,并且仅用于小部分功能。那么它实际上可能对大多数用户感知性能的模块有明显影响。

  • 使用通常理解的工具来定义公共API,我的意思是__all__变量。保持它最新可能有点烦人,但检查所有函数是否存在过时的导入或添加新函数时添加所有相关导入也是如此。从长远来看,通过更新__all__您可能需要做更少的工作。

  • 你喜欢哪个真的不重要,两者都可以工作。如果你独自工作,你可以考虑利弊并选择你认为最好的那个。然而,如果你在团队中工作,你可能应该坚持已知模式(即带有__all__的顶级导入),因为它允许他们做他们(可能)一直在做的事情。


3

我认为在某些情况下这是一种推荐的方法。例如,在Google App Engine中,推荐懒加载大模块,因为它将最小化实例化新Python VMs/解释器的预热成本。可以查看Google工程师的演示文稿来了解详细信息。但是请记住,这并不意味着您应该懒加载所有模块。


3
人们已经很好地解释了为什么要避免内联导入,但并没有真正提供替代工作流程来解决你最初想要它们的原因。

我很难在源文件中上下滚动以找出可用的模块导入等信息。

为了检查未使用的导入,我使用pylint。它对Python代码进行静态(近似)分析,并检查许多内容之一是未使用的导入。例如,以下脚本..

import urllib
import urllib2

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

..将生成以下消息:

example.py:2 [W0611] Unused import urllib2

关于检查可用的导入,我通常依赖TextMate的(相当简单的)完成功能-当您按下Esc时,它将使用文档中的其他单词来完成当前单词。如果我已经执行了import urlliburll[Esc]将扩展为urllib,否则我会跳转到文件开头并添加导入。

2

一个建议:通过将两个模块都需要的所有内容放入第三个模块中来打破依赖循环。让这两个模块都导入这个第三个模块。 - nosklo
@nosklo:非常好的建议。通过重构,在Python中打破依赖循环是微不足道的。 - S.Lott

1
你可能想要查看 Python 维基上的 statement overhead 导入。简而言之:如果模块已经被加载(查看 sys.modules),你的代码将运行得更慢。如果你的模块还没有被加载,且只有在需要时才会加载 foo,这可能是零次,那么整体性能将会更好。

2
-1 代码不一定会运行得更慢。请参考我的回答 - aaronasterling

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接