两种变体都有它们的用途。然而,在大多数情况下,最好在函数外部导入,而不是在函数内部。
性能
已经在几个答案中提到了这一点,但我认为它们都缺乏完整的讨论。
第一次在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)
%timeit loopy(func_2, 10000)
那几乎慢了两倍。
非常重要的是要意识到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)
%timeit f2(10000)
%timeit f3(10000)
虽然你可以清楚地看到反复查找全局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)
%timeit f5(10000)
速度再次提升,但是导入和变量之间几乎没有区别。
可选依赖项
有时候模块级别的导入实际上可能会成为问题。例如,如果您不想添加另一个安装时的依赖关系,但该模块对于一些额外的功能非常有用。决定依赖关系是否应该是可选的不应该轻率地进行,因为它会影响用户(无论是他们是否遇到意外的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()
这可以通过提供替代实现或自定义异常(或消息)来更加定制化,但这是主要内容。
如果想为可选依赖项提供另一种“解决方案”,则顶层方法可能会更好,但通常人们使用函数内导入。大多数情况下,这会导致更清洁的堆栈跟踪并且更短。
循环导入
在函数内导入可以非常有助于避免由于循环导入而导致的 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__
的顶级导入),因为它允许他们做他们(可能)一直在做的事情。