在Python中,当你在一个函数内部导入(import)时会发生什么?

265

在速度和内存效率方面,将Python模块和/或函数导入到函数内部的优缺点是什么?

每次运行函数时是否会重新导入,还是只会在开始导入一次,无论是否运行函数?


5
即使模块已经被加载,调用“import”也非常耗时,所以它并没有速度上的优势。如果你想获得速度上的优势,最好的方法是在函数中将模块作为第一件事赋值给一个本地变量,然后通过该本地变量访问它(因为本地变量查找非常快),前提是你至少要访问该模块4-5次。 - Nick Bastin
1
@Tim:优化模块访问速度(假设你正尝试这样做,并且你访问模块的次数足够多,使得本地赋值有意义)的最佳方式是在文件级别上像往常一样 import 模块,然后在函数内部将模块分配给本地变量。为了使分配有意义,在函数内部至少需要访问该模块 4 次-如果您使用的频率比这还要低,那么在全局级别进行直接的模块. 符号查找与本地赋值/查找相比不会更慢。 - Nick Bastin
1
@NickBastin 五年半后,将模块分配给本地变量仍然是一种优化吗? - 2rs2ts
@2rs2ts:是的 - 本地变量是您可以拥有的最快访问方式。即使您有一个循环,其中包含大量迭代并且需要消耗大量时间,我仍不建议这样做。 - Nick Bastin
4
@NickBastin,你的PyCon链接已经失效。 - Navin
显示剩余3条评论
6个回答

243

每次运行函数时,它是否会重新导入?

不会;或者更确切地说,Python模块在每次导入时都会被缓存,因此第二次(或第三次、第四次……)导入实际上并不会强制其再次经历整个导入过程。1

它是否在开始时导入,无论函数是否运行?

不是的,只有当函数被执行时才会导入。2, 3

关于好处:这取决于情况,我想。如果您只需要很少运行一个函数,并且不需要在其他任何地方导入该模块,则仅在该函数中导入它可能会有益。或者如果存在名称冲突或其他原因,您不希望该模块或该模块中的符号在所有地方都可用,则可能只想在特定函数中导入它。(当然,在这些情况下,总是可以使用from my_module import my_function as f。)
在一般实践中,它可能没有那么有益。事实上,大多数Python风格指南鼓励程序员将所有导入放在模块文件的开头。

46
同样的思路,如果导入被嵌套在辅助函数中,那么可以使依赖项变成可选的。 - M. Toya
6
我在为自己编写库模块时,会使用它来处理可选依赖关系。我让库模块内的每个函数都依赖最小数量的导入。 - CodeMonkey
3
谢谢!把加载较慢的模块 import plotly 放在调用它的函数内部,大大减少了我的 web2py 应用程序加载时间。 - laviex
2
我发现有时需要 包A 来完成某个任务。过后我重新编写代码,决定使用 包B。此时,我可能不记得在我的项目中是否还在其他地方使用了 包A ,这样就可能导致我在不再需要它的情况下仍未将其移除,或者在仍需要它的情况下已经将其移除(如果它是代码中很少使用的部分,则我可能不能立即注意到该问题)。我想知道其他人是否有避免这种问题的方法,特别是在Pythonic的情况下,不在函数内导入模块? - PatrickT
3
如果您使用的是IDE,它们往往会提示您是否未使用某个导入项。例如,在VSCode中,Pylance会告诉我是否未访问某个导入项。 - Kraigolas

59
第一次从任何地方(函数内或外)导入goo时,goo.py(或其他可导入形式)将被加载,并将sys.modules['goo']设置为所构建的模块对象。在程序运行的同一次中进行的任何未来导入(再次在函数内或外部),只需查找sys.modules['goo']并将其绑定到适当作用域中的裸名goo。字典查找和名称绑定都是非常快速的操作。

假设第一个import在程序运行期间完全摊销,那么将“适当作用域”设置为模块级别意味着每个goo.thisgoo.that等的使用都需要进行两次字典查找——一次是goo的查找,一次是属性名称的查找。将其设置为“函数级别”每次运行函数时需要进行一个额外的局部变量设置(比字典查找部分更快!),但可以为每个goo.this(等等)访问节省一个字典查找(用本地变量查找交换,非常快),从而基本上减少这些查找所需的时间。

我们谈论的是微秒级别的差异,因此这几乎不是一项值得优化的工作。将import放在函数内的一个潜在的重大优点是,该函数在给定程序运行中可能根本不需要,例如,该函数处理错误、异常和一般稀有情况;如果是这种情况,则任何不需要该功能的运行都不会执行导入(这是微秒级别而不仅是纳秒级别的节省),只有需要该功能的运行才会支付(适度但可测量的)代价。

这仍然是一种只在极端情况下才值得的优化方法,在尝试挤出微秒级时间之前,还有许多其他方法可以考虑。


这种优化实际上并不值得 - 即使模块已经加载,调用“import”所带来的巨大开销也无法弥补快速访问本地变量的任何数量。检查模块是否已加载是一项非常昂贵的操作(相对于几个全局字典查找)。 - Nick Bastin
6
首次导入模块的成本较高。尝试运行一个空脚本和一个仅包含 import string, itertools, fractions, heapq, re, array, bisect, collections, math, os 的脚本进行比较。前者平均需要 180 毫秒,而后者需要 230 毫秒。因此,这不是微秒级别的差别,而是数十毫秒(可能是磁盘访问发生的原因?)。对于多次运行的小型脚本(例如服务网络请求),这一点很重要。 - Evgeni Sergeev
1
@EvgeniSergeev 在这种情况下,通常会有一个始终运行的服务器,因此它不会一遍又一遍地重新导入。 - Tobias Kienzler
3
在FaaS(函数即服务)环境和/或某些低性能(例如嵌入式)设备中,这仍然适用。论点是延迟的差异可能是显著的,不能简单地忽略。 - user3576508

22

该函数执行第一次时只会导入一次。

优点:

  • 只导入与函数相关的模块
  • 方便将函数移动到包中的其他位置

缺点:

  • 无法看出该模块可能依赖哪些模块

3
如果你执行类似grep import /path/to/module这样的命令,它将显示该模块导入的所有模块。 - user1465368

11

我建议您不要问“X会提高我的性能吗?”这样的问题,而是使用性能分析工具确定程序花费时间的实际情况,并根据您可以获得最大收益的地方进行优化。

然后,您可以使用性能分析工具来确保您的优化确实带来了效益。


5
我同意这一点,这只是一个好奇的问题。我想知道Python的导入方法是如何工作的,比试图进行过早性能优化更详细。谢谢 :) - Tim McJilton
3
好的,希望这里优秀的回答已经满足了您的好奇心!Effbot网站上有一些信息可能对您有用:http://effbot.org/zone/import-confusion.htm向下滚动到“Python导入模块时做了什么?” - gomad
感谢提供的信息,这些答案非常出色并且有所帮助。 - Tim McJilton

8

在函数内部导入模块将会在第一次运行该函数时有效地导入模块。

无论您是在顶部导入还是在函数运行时导入,它都应该同样快速地导入。这通常不是在def中导入的好理由。优点是什么?如果未调用该函数,则不会导入。如果您的模块只需要用户安装某个特定的模块来使用您的特定函数,那么这实际上是一个合理的原因...

如果这不是您这样做的原因,那几乎肯定是一个糟糕的想法。


4

当函数第一次被调用时,它会进行一次导入。

如果我有一个在导入模块中很少使用且是唯一需要导入的函数,我可以想象以这种方式执行。虽然看起来有些牵强...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接