这两种变体都有其用途。但是在大多数情况下,最好在函数外部而不是内部导入。
性能
在几个答案中都提到过,但在我看来,他们都缺乏完整的讨论。
第一次在 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 或者错过了“很酷的功能”)并且它使得安装具有所有功能的包更加复杂, 对于普通依赖 pip 或 conda(仅提及两个包管理器)开箱即用,但对于可选依赖,用户必须稍后手动安装包(有一些选项可以自定义要求,但“正确”安装它的负担再次落在了用户身上)。
但同样可以通过两种方式完成:
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__ 的顶级导入),因为它允许他们做他们(可能)一直在做的事情。