两种变体都有其用途。但是,在大多数情况下,最好在函数外部而不是在函数内部导入。
性能
在几个答案中都提到了它,但我认为他们都没有完整的讨论。
第一次将模块导入python解释器时,无论是在顶层还是在函数内部,它都会很慢。这很慢,因为Python(我专注于CPython,其他Python实现可能有所不同)执行了多个步骤:
- 找到包。
- 检查软件包是否已转换为字节码(著名
__pycache__
目录或.pyx
文件),如果不是,则将其转换为字节码。
- Python加载字节码。
- 加载的模块放入
sys.modules
。
随后的导入不必执行所有这些操作,因为Python可以简单地从返回模块sys.modules
。因此,随后的进口将更快。
可能是模块中的某个功能实际上并不经常使用,但它依赖于import
花费很长时间的功能。然后,您实际上可以import
在函数内部移动。这样可以更快地导入模块(因为不必立即导入长加载包),但是最终使用该函数时,第一次调用时会很慢(因为必须导入模块)。这可能会影响感知的性能,因为您不会减慢所有用户的速度,而只会减慢使用依赖于慢速加载依赖性的功能的用户。
但是,查找sys.modules
不是免费的。它非常快,但不是免费的。因此,如果您实际上import
经常调用一个sa包的函数,则会发现性能略有下降:
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)
再次加快了速度,但是import和变量之间几乎没有区别。
可选依赖项
有时进行模块级导入实际上可能是一个问题。例如,如果您不想添加其他安装时依赖性,但是该模块对于某些其他功能确实很有帮助。确定依赖项是否应该是可选的,不应该轻易做,因为它将影响用户(如果他们遇到意外情况,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__
),因为它允许他们执行(可能)一直做的事情。