为什么一个缺失但并非必需的父级会导致动态模块重新加载失败?

编程语言 2026-07-11

当我在动态导入上使用 importlib.reload 时,出现了一个错误。动态导入成功了,但紧接着的 importlib.reload() 失败,提示没有父包。我到底哪里做错了?

我使用的是Python 3.12.12。

命令提示符:

C:\tmp\bugs\python\importhell> python hmm.py test_in phobia.agoraphobia QQ
Run 0
Importing phobia.agoraphobia from test_in\phobia/agoraphobia.py
QQ from phobia.agoraphobia = 12345
Run 1
Traceback (most recent call last):
  File "C:\tmp\bugs\python\importhell\hmm.py", line 32, in <module>
    doit(args)
  File "C:\tmp\bugs\python\importhell\hmm.py", line 22, in doit
    module = get_module(args.module, args.rootdir)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\tmp\bugs\python\importhell\hmm.py", line 16, in get_module
    importlib.reload(module)
  File "C:\Users\{redacted}\.conda\envs\py3datalab\Lib\importlib\__init__.py", line 121, in reload
    raise ImportError(f"parent {parent_name!r} not in sys.modules",
ImportError: parent 'phobia' not in sys.modules

hmm.py:

import argparse
import importlib.util
import os
import sys

def get_module(qualname, rootdir):
    module = sys.modules.get(qualname)
    if module is None:
        modulepath = os.path.join(rootdir, '/'.join(qualname.split('.'))+'.py')
        print("Importing %s from %s" % (qualname, modulepath))
        spec = importlib.util.spec_from_file_location(qualname, modulepath)
        module = importlib.util.module_from_spec(spec)
        sys.modules[qualname] = module
        spec.loader.exec_module(module)
    else:
        importlib.reload(module)
    return module

def doit(args):
    for run in range(2):
        print("Run %d" % run)
        module = get_module(args.module, args.rootdir)
        value = getattr(module, args.name)
        print('%s from %s = %s' % (args.name, args.module, value))

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('rootdir')
    parser.add_argument('module')
    parser.add_argument('name')
    args = parser.parse_args()
    doit(args)

文件系统包含:

- hmm.py
- test_in
  - phobia
    - agoraphobia.py

文件agoraphobia.py只包含这一行 QQ = 12345

解决方案

为什么缺失但并非必须的父包会导致动态模块重新加载失败?

如果你查看 importlib 的源代码:

def reload(module):
    """Reload the module and return it.

    The module must have been successfully imported before.

    """
    try:
        name = module.__spec__.name
    except AttributeError:
        try:
            name = module.__name__
        except AttributeError:
            raise TypeError("reload() argument must be a module") from None

    if sys.modules.get(name) is not module:
        raise ImportError(f"module {name} not in sys.modules", name=name)
    if name in _RELOADING:
        return _RELOADING[name]
    _RELOADING[name] = module
    try:
        parent_name = name.rpartition('.')[0]
        if parent_name:
            try:
                parent = sys.modules[parent_name]
            except KeyError:
                raise ImportError(f"parent {parent_name!r} not in sys.modules",
                                  name=parent_name) from None
            else:
                pkgpath = parent.__path__
        else:
            pkgpath = None
        target = module
        spec = module.__spec__ = _bootstrap._find_spec(name, pkgpath, target)
        if spec is None:
            raise ModuleNotFoundError(f"spec not found for the module {name!r}", name=name)
        _bootstrap._exec(spec, module)
        # The module may have replaced itself in sys.modules!
        return sys.modules[name]
    finally:
        try:
            del _RELOADING[name]
        except KeyError:
            pass

如果模块的名称中包含 .,那么 importlib.reload 会假设父包存在,并试图使用它来找到用于加载该模块的规范,以便重新加载它。


为解决它:

  1. 创建一个空的 test_in/phobia/__init__.py 文件,以使 phobia 成为一个有效的包。
  2. 在从 test_in 目录重新加载 phobia.agoraphobia 之前,确保已经加载了 phobia 包,可以通过加载 phobia.__init__ 来实现。
import argparse
import importlib
import os
import sys


def get_module(name, path, rootdir):
    module = sys.modules.get(name)
    if module is None:
        modulepath = os.path.join(rootdir, os.path.sep.join(path.split('.'))+'.py')
        print("Importing %s from %s" % (name, modulepath))
        spec = importlib.util.spec_from_file_location(name, modulepath)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        sys.modules[name] = module
    else:
        module = importlib.reload(module)
    return module


def doit(args):
    rootdir = args.rootdir

    if args.load_ancestor:
        parent = args.module.rpartition(".")[0]
        if parent:
            print(f"Loading parent module {parent}")
            get_module(parent, f"{parent}.__init__", rootdir)

    for run in range(2):
        print(f"Run {run}")
        module = get_module(args.module, args.module, rootdir)
        value = getattr(module, args.name)
        print(f"{args.name} from {args.module} = {value}")


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-l", "--load_ancestor", action="store_false")
    parser.add_argument("rootdir", nargs="?", default="test_in")
    parser.add_argument("module", nargs="?", default="phobia.agoraphobia")
    parser.add_argument("name", nargs="?", default="QQ")
    args = parser.parse_args()
    doit(args)

输出如下:

Loading parent module phobia
Importing phobia from test_in\phobia\__init__.py
Run 0
Importing phobia.agoraphobia from test_in\phobia\agoraphobia.py
QQ from phobia.agoraphobia = 12345
Run 1
QQ from phobia.agoraphobia = 12345
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章