Python 导入编码风格

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

基本上,我花了很长时间来清理源文件,以确定哪些模块导入是可用的等等

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

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

30215 次浏览

这种方法有几个问题:

  • 在打开文件时,它所依赖的模块并不是很明显。
  • 它会混淆那些需要分析依赖关系的程序,比如 py2exepy2app等等。
  • 您在许多函数中使用的模块怎么样?您可能会导致大量冗余导入,或者必须在文件顶部使用一些导入函数和一些内部函数。

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

在某些情况下,我发现函数内部的导入是有用的:

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

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

这确实有一些缺点。

测试

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

import mymodule
mymodule.othermodule = module_stub

你必须这么做

import othermodule
othermodule.foo = foo_stub

这意味着您必须在全局范围内修补其他模块,而不是仅仅更改 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

从性能角度来看,您可以看到: Python 导入语句应该总是位于模块的顶部吗?

通常,我只使用本地导入来打破依赖周期。

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

在“已删除的语法”下面有一个简短的提及:

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

我相信在某些情况/场景下,这是一种推荐的方法。例如,在 Google App Engine 中,建议延迟加载大模块,因为这样可以最小化实例化新 Python VM/解释器的预热成本。看一下描述这一点的 谷歌工程师演示文稿。但是请记住,这个 没有意味着您应该延迟加载所有模块。

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

我建议你尽量避免进口 from foo import bar。我只在包中使用它们,其中分割成模块是一个实现细节,而且不会有很多模块。

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

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

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

我花了很长时间来清理源文件,以弄清楚哪些模块导入是可用的,等等

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

import urllib
import urllib2


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

. . 将产生以下信息:

example.py:2 [W0611] Unused import urllib2

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

这个问题的(前面的) 最佳答案格式很好,但是在性能方面完全错误

表演

顶级进口

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 世界中的大多数其他内容是一致的。

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

表演

这个问题在好几个答案中都提到过,但在我看来,它们都缺乏一个完整的讨论。

第一次在 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)

差不多慢了两倍。

认识到 答案中有点“作弊”的意思。他指出,在函数中进行导入实际上会使函数运行得更快。在某种程度上,这是真的。这是因为 Python 是如何查找名称的:

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

因此,与检查局部作用域并检查全局作用域相反,检查局部作用域就足够了,因为模块的名称在局部作用域中是可用的。这样反而更快了!但这是一种叫做 “循环不变码运动”的技术。它基本上意味着,通过在循环(或重复调用)之前将其存储在变量中,可以减少在循环(或重复调用)中执行的操作的开销。因此,与其在函数中使用 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)

同样快得多,但是导入和变量之间几乎没有区别。

可选的依赖项

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

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

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

循环进口

函数内导入非常有助于避免循环导入导致的“导入错误”。在很多情况下,循环导入是“糟糕”包结构的标志,但是如果完全没有办法避免循环导入,那么“循环”(以及由此产生的问题)就可以通过将导致循环的导入放在实际使用它的函数中来解决。

别再说了

如果实际上将所有导入放在函数而不是模块作用域中,就会引入冗余,因为函数很可能需要相同的导入。这有几个缺点:

  • 您现在有多个地方可以检查是否有任何导入已经过时。
  • 如果您拼错了某个导入,那么只有在运行特定函数时,而不是在加载时才会发现。因为有了更多的 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__的顶级导入) ,因为它允许他们做他们(可能)一直在做的事情。