Python import机制探究

最近遇到一个需求,需要动态加载一堆 Python 模块,但是这些 .py 是 pb 文件生成的,无法控制命名,导致导入的时候各种报错。

目录结构

package1.py 内容

package2.py 内容

package3.py 内容

可以看见在 basic.package1 中导入了 package2、package3。我们在 main 文件中导入 basic 包。

这样肯定不行,报错了:ModuleNotFoundError: No module named 'basic'

sys.path

Google 一下或者稍微了解 Python 的就会知道,Python 导入非系统库需要指定 PYTHONPATH,Python 启动时会读取此环境变量并加入到 sys.path 中。我们可以打印下:

可以看到默认加载了当前项目目录、Python系统目录以及 dist-packages(通过 pip 安装的),我们把 temp_dir 目录加进去就行了。代码增加:

但还是会报错:ImportError: cannot import name 'package2' from 'calendar' (/usr/lib/python3.8/calendar.py),不过至少说明,加载路径是设置成功了,basic 包本身导入成功了,只是 from calendar import package2 执行失败了。

这个是一个经典错误,搞 Python 的都会遇到,那就是自己定义的包名不能和系统的重复,一般是改个名字或者加上命名空间,但是我们这个场景中,Python 文件是 pb 生成的,无法修改源文件,修改生成的文件也比较麻烦。也能解决,就是提高我们路径的优先级,改成 sys.path.insert(0, temp_dir) 加载完了再改回来。

BuiltinImporter

不过又有新的报错:ImportError: cannot import name 'package3' from 'math' (unknown location),有输出 import package2,看着还是导入了系统的 math 库,没有导入自己的,还没有显示路径。这里匪夷所思,卡了很久,继续单步调试代码和查阅资料,发现 Python 其实是支持多种导入方式,甚至可以自定义。importlib._bootstrap._find_spec 中,是循环 sys.meta_path 中的 Importer 哪个找到算哪个。我们可以打印下 sys.meta_path:

可以看见最前面有个 BuiltinImporter(具体是在 importlib._bootstrap._install 时注入的),唉,灵光一闪,math 就是一个内建库呀,这里结合源码猜测,C 的库(详见 sys.builtin_module_names)都是通过这个导入的。去掉这个就能导入成功了(记得加载完了还原回来)。

终于成功了:

sys.modules

不过,这里只是个 demo。实际情况比这个复杂,还是会报 ImportError: cannot import name 'package2' from 'calendar' (/usr/lib/python3.8/calendar.py),这里很奇怪,已经保证了加载的优先级,但为什么还不行呢?没办法,扒一下源码,终于找到了一点端倪。importlib._bootstrap._find_and_load 方法中,会先在 sys.modules 里面找,如果有则直接返回,官方文档也有相应说明。

所以还得排除已加载的情况

最终到此才彻底解决,完整 demo 代码:https://github.com/iyaozhen/python-import-test

参考资料

https://docs.python.org/3/library/importlib.html

妙用Hook来研究Python的Import机制,https://www.51cto.com/article/527713.html

Python3.7源码剖析之import,https://zhuanlan.zhihu.com/p/361720373