为什么需要在函数内部导入
一般来说,我们都是在文件的头部进行导入,常见的形式有:
import A
from A import Func
这两种导入方式对应的底层逻辑是一致的。
如果我们导入的是模块名,那么当文件被加载时会根据模块名从sys.modules
中查询,如果对应的模块还未被初始化,那么首先就会进行模块的初始化。如果模块已经初始化完,那么就会把对应模块对象的引用拷贝当前的名字空间,即拷贝到globals()
字典中,对应模块对象的引用计数会 + 1。后续当我们再从当前文件使用导入的模块时会直接从名字空间中查找,也就是说引用的拷贝只会发生在文件的第一次加载,因此具有较高的效率。
而如果我们导入的是模块中的类/函数/属性等,那么会先根据模块名查找到模块对象,再从模块对象的__dict__
数据结构中查找到目标类对象/函数对象,然后把这个类对象/函数对象的引用拷贝到当前名字空间。后续再使用导入的类/函数时也不会再次导入,因此也是具有较高的效率。
这种导入方式还有一个直观的优点在于它能明确的展示出文件的逻辑依赖关系,方便进行项目代码管理。
所以,这种导入方式是我们在实际开发中优先推荐使用的导入方式。
但是这种导入方式实际也有一个缺点,就是当有多个文件互相导入时会有 循环导入 的问题,相信大家都遇到过这个问题,也清楚造成这种问题的原因,这里就不再赘述。
另一种不使用这种导入方式的情景是当我们想要根据不同配置导入不同的模块,这种使用方式常常是由于文件所依赖模块不一定存在当前工作目录,因此把这部分的实现留给了使用者自行实现。比方说基础架构在内部组件互相调用时,可能会提供这样的可重载函数:
@delegate
def SendByRPC(iServer, *args, **kwargs):
import grpc
grpc.CallFunc(iServer, None, *args, **kwargs)
项目组可能没有接入grpc
,那么就可以重载上面的接口,使用项目自己的 rpc 组件
函数内部导入的性能
比方说我们有如下的代码:
def Func():
import A
A.Update()
当上述代码被运行时,它的执行逻辑如下:
- 根据模块名
A
从sys.modules
中查找模块对象; - 如果模块对象未初始化,则先进行初始化后放入
sys.modules
中,然后返回模块对象的引用 - 模块对象
A
的引用计数+1; - 从模块对象
A
的名字空间内找到Update
函数对象并执行; - 执行完毕,函数栈退出,销毁
locals()
局部名字空间,模块对象A
的引用计数-1
可以看到上面的流程本质上和在文件头部导入的流程是一样的,只不过从文件头部导入后会保存在文件的globals
全局名字空间,而函数内部的导入则只保存在函数的locals
局部名字空间,而这个空间每当函数退出就会被回收,所以每次运行都需要进行引用的拷贝,这也是导致性能较低的原因。
经过测试,对于一个简单的函数调用(函数主体简单),从函数内部 导入模块 性能会低 20% 左右。如果我们使用的导入形式是from A import Update
的形式,由于每次导入需要额外查找函数对象,则性能将会大幅降低,达到 300% 以上,严重影响函数运行效率。
因此在实际编码过程中,我们要尽量避免使用函数内部导入的方式。
提升函数内部导入的性能
虽然函数内部导入性能不佳,但是有时候遇到了循环导入问题,还是不得不使用这种方式。那么有什么方法可以提升导入性能呢?
分析从文件头部导入的方式,之所以后续不再需要重复导入,是因为模块的引用被保存在了globals
全局名字空间,因此能够想到的一种解决方式就是把从函数内部导入的模块对象引用也保存到全局名字空间里,当函数再次调用时,直接从全局名字空间获取,这样我们就“模拟”出了文件头部导入的方式。
如以下代码所示:
# 导入模块
if "A" not in globals():
A = None
def Func():
global A
if not A:
import A
A = A
A.Update()
# 导入函数
if "Update" not in globals():
Update = None
def Func():
global Update
if not Update:
from A import Update
Update = Update
Update()
经过测试,这种方式的运行效率和从文件头部直接导入几乎相当,在函数被第一次运行之后,会在globals
全局名字空间保存模块对象/函数对象的引用,而且如果设置的键名和原始模块/函数对象名一致,那么效果就完全和从文件头部直接导入相同。