• Python导入模块时报错:ModuleNotFoundError: No module named 'basic'
  • 发布于 1周前
  • 59 热度
    0 评论
  • 怪咖豆
  • 0 粉丝 23 篇博客
  •   
最近遇到一个需求,需要动态加载一堆 Python 模块,但是这些 .py 是 pb 文件生成的,无法控制命名,导致导入的时候各种报错。

目录结构
├── main.py
└── temp_dir
    ├── __init__.py
    ├── basic
    │   ├── __init__.py
    │   └── package1.py
    ├── calendar
    │   ├── __init__.py
    │   └── package2.py
    └── math
        ├── __init__.py
        └── package3.py
package1.py 内容
from calendar import package2
from math import package3
 
print("import package1")
package2.py 内容
print("import package2")
package3.py 内容
print("import package3")
可以看见在 basic.package1 中导入了 package2、package3。我们在 main 文件中导入 basic 包。
import importlib
 
if __name__ == '__main__':
    print(importlib.import_module("basic.package1"))
这样肯定不行,报错了:ModuleNotFoundError: No module named 'basic'

sys.path
Google 一下或者稍微了解 Python 的就会知道,Python 导入非系统库需要指定 PYTHONPATH,Python 启动时会读取此环境变量并加入到 sys.path 中。我们可以打印下:
['/mnt/c/Users/PycharmProjects/python-import-test', 
'/usr/lib/python38.zip', 
'/usr/lib/python3.8', 
'/usr/local/lib/python3.8/dist-packages']
可以看到默认加载了当前项目目录、Python系统目录以及 dist-packages(通过 pip 安装的),我们把 temp_dir 目录加进去就行了。代码增加:
temp_dir = "%s/temp_dir" % os.path.dirname(__file__)
sys.path.append(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:
[<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>]
可以看见最前面有个 BuiltinImporter(具体是在 importlib._bootstrap._install 时注入的),唉,灵光一闪,math 就是一个内建库呀,这里结合源码猜测,C 的库(详见 sys.builtin_module_names)都是通过这个导入的。去掉这个就能导入成功了(记得加载完了还原回来)。
builtin = None
get_class_name = lambda _class: _class.__name__ if hasattr(_class, '__name__') else type(_class).__name__
for importer in sys.meta_path:
    if get_class_name(importer) == 'BuiltinImporter':
        builtin = importer
        break
if builtin:
    sys.meta_path.remove(builtin)
终于成功了:
import package2
import package3
import package1
<module 'basic.package1' from 'python-import-test/temp_dir/basic/package1.py'>
sys.modules
不过,这里只是个 demo。实际情况比这个复杂,还是会报 ImportError: cannot import name 'package2' from 'calendar' (/usr/lib/python3.8/calendar.py),这里很奇怪,已经保证了加载的优先级,但为什么还不行呢?没办法,扒一下源码,终于找到了一点端倪。importlib._bootstrap._find_and_load 方法中,会先在 sys.modules 里面找,如果有则直接返回,官方文档也有相应说明。
def _find_and_load(name, import_):
    """Find and load the module."""
    with _ModuleLockManager(name):
        module = sys.modules.get(name, _NEEDS_LOADING)
        if module is _NEEDS_LOADING:
            return _find_and_load_unlocked(name, import_)
所以还得排除已加载的情况
for exclude in ["calendar", "math"]:
    if exclude in sys.modules:
        del sys.modules[exclude]
最终到此才彻底解决,完整 demo 代码:https://github.com/iyaozhen/python-import-test
用户评论