Python 导入编码风格

Posted

技术标签:

【中文标题】Python 导入编码风格【英文标题】:Python import coding style 【发布时间】:2010-10-03 09:19:57 【问题描述】:

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

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

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__ 变量,但是随着模块的发展,我必须对其进行更新,这无助于实际存在于模块中的代码的命名空间污染。

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

【问题讨论】:

Should Python import statements always be at the top of a module?的可能重复 【参考方案1】:

这个问题的(以前的)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 用于引用实际的字节码差异......如果我要在函数中经常使用它们(更清晰的字节码和更清晰的代码),有时我会在方法中使一些类属性本地化/跨度> 进行导入确实有不小的惩罚。这被本地访问和函数中存在的循环混淆了。如果您在 Top Import 示例中添加“r = random”并使用 r.random(),您将获得与第二个相同的性能。如果你添加“r = random.random”并使用“r()”,它会更快。 @HKrishnan,完全正确。我想我不够清楚。如果模块被访问很多次,那么在函数中导入会更快。除非多次调用导入的函数,否则您的建议会更快,但只会略微提高。总的来说,我提倡在模块级别导入。在函数中导入时我能想到的一种情况是最佳的,即在程序的正常执行过程中预计不会调用该函数并且具有唯一的导入。 Django 视图就是一个很好的例子。 如果您出于性能原因只想使用LOAD_FAST,请在全局级别设置import random,然后在本地范围设置random_ = random,并使用random_(或者更好的是,使用@ 保存一些属性查找987654331@ 或 from random import random)。仅仅为了使用 LOAD_FAST 而在每个函数调用上吃掉导入的性能损失是个坏主意。【参考方案2】:

这确实有一些缺点。

测试

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

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

【讨论】:

你可能想澄清一下——每次都会检查导入,但模块只加载一次。 感谢您的意见。即使模块被缓存,它仍然确实会产生很大的性能影响,正如您从我的测试中看到的那样。 是的,但现在你说清楚了。这非常具有误导性。删除了我的反对票 这不是一个很好的例子,因为您将 import 放在 for 循环中,而不是放在 f() 的定义中。但是,是的,一般来说,本地进口确实是有成本的。 @Ryan -1。在循环内输入 import 语句比在循环外输入更容易吗?你对性能的回答是绝对错误的。请参阅mine。【参考方案3】:

这种方法的一些问题:

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

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

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

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

另外:将导入放在每个函数中实际上并不明显比文件顶部慢。第一次加载每个模块时,将其放入sys.modules,随后的每次导入仅花费查找模块的时间,这相当快(不会重新加载)。

【讨论】:

+1:而且速度很慢。每个函数调用都必须重复导入模块检查。【参考方案4】:

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

这里的“删除的语法”下有一个简短的提及:

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

【讨论】:

-1:错误。只有“from xxx import *”表单已从函数中禁用。 他说部分的导入已被禁用。不要这么快就对提供有用信息的人投反对票。【参考方案5】:

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

在您导入包的所有其他地方,只需使用import foo,然后通过全名foo.bar 引用它。通过这种方式,您始终可以知道某个元素的来源,而不必维护导入元素的列表(实际上这将始终过时并导入不再使用的元素)。

如果foo 是一个很长的名字,你可以用import foo as f 简化它,然后写成f.bar。这仍然比维护所有 from 导入更加方便和明确。

【讨论】:

【参考方案6】:

人们已经很好地解释了为什么要避免内联导入,但并没有真正替代工作流程来解决您首先需要它们的原因。

我很难上下清理源文件以找出可用的模块导入等等

为了检查未使用的导入,我使用pylint。它对 Python 代码进行静态(ish)分析,它检查的(许多)事情之一是未使用的导入。比如下面的脚本..

import urllib
import urllib2

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

..将生成以下消息:

example.py:2 [W0611] Unused import urllib2

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

【讨论】:

【参考方案7】:

我相信在某些情况下这是推荐的方法。例如,在 Google App Engine 中,建议延迟加载大模块,因为它可以最大限度地减少实例化新 Python 虚拟机/解释器的预热成本。查看描述此内容的Google Engineer's 演示文稿。但是请记住,这并不意味着您应该延迟加载所有模块。

【讨论】:

【参考方案8】:

从性能上看,可以看到:Should Python import statements always be at the top of a module?

一般来说,我只使用本地导入来打破依赖循环。

【讨论】:

一个建议:通过将两个模块需要的所有东西放入第三个模块来打破依赖循环。让两个模块都导入第三个。 @nosklo:很好的建议。通过重构打破 Python 中的依赖循环是微不足道的。【参考方案9】:

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

性能

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

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

找到包裹。 检查包是否已经转换为字节码(著名的__pycache__ 目录或.pyx 文件),如果没有,则将这些转换为字节码。 Python 加载字节码。 加载的模块放在sys.modules中。

后续导入不必执行所有这些操作,因为 Python 可以简单地从 sys.modules 返回模块。所以后续的导入会快很多。

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

但是sys.modules 中的查找不是免费的。它非常快,但它不是免费的。因此,如果您实际上经常调用 imports 包的函数,您会注意到性能略有下降:

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" 的技术。它基本上意味着您可以通过在循环(或重复调用)之前将其存储在变量中来减少循环(或重复)中完成的某些事情的开销。因此,除了在函数中importing 它,您还可以简单地使用一个变量并将其分配给全局名称:

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)

又快了很多,但导入和变量之间几乎没有区别。

可选依赖项

有时具有模块级导入实际上可能是一个问题。例如,如果您不想添加另一个安装时依赖项,但该模块对某些 additional 功能非常有帮助。决定一个依赖项是否应该是可选的不应该轻易完成,因为它会影响用户(如果他们得到一个意想不到的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()

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

如果想要为可选依赖项提供替代“解决方案”,***方法可能会更好一些,但通常人们使用函数内导入。主要是因为它会导致更清晰的堆栈跟踪并且更短。

循环导入

函数内导入对于避免由于循环导入而导致的 ImportErrors 非常有帮助。在很多情况下,循环导入是“坏”包结构的标志,但如果绝对没有办法避免循环导入,则“循环”(以及问题)可以通过将导致循环的导入放在里面来解决实际使用它的函数。

不要重复自己

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

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

其他想法:

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

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

我很少不小心用其他模块的内容污染我的模块

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

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

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

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

总结

性能影响是可见的,但几乎总是会进行微优化,因此不要让微基准来指导您决定导入的位置。除非依赖项在第一个 import 上真的很慢,并且它仅用于一小部分功能。然后它实际上会对大多数用户的模块感知性能产生明显的影响。

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

你喜欢哪一个并不重要,两者都可以。如果你一个人工作,你可以考虑利弊,然后做你认为最好的一个。但是,如果您在一个团队中工作,您可能应该坚持使用已知模式(这将是使用 __all__ 的***导入),因为它允许他们做他们(可能)一直在做的事情。

【讨论】:

【参考方案10】:

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

【讨论】:

-1 代码不一定会运行得更慢。见我的answer。【参考方案11】:

安全实施

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

【讨论】:

以上是关于Python 导入编码风格的主要内容,如果未能解决你的问题,请参考以下文章

(转)PEP 8——Python编码风格指南

Python 编码风格参考

python基础之类的编码风格

Python的常见编码风格?

Python的编码风格

python 小插曲:编码风格