2025, Dec 22 06:02

Почему inspect не видит классы при динамическом импорте и как это исправить

Почему inspect.getsource и getsourcefile ломаются на классах при динамическом импорте Python, и как исправить TypeError, зарегистрировав модуль в sys.modules

Динамические импорты — удобный способ загружать модули Python по пути к файлу, однако иногда они конфликтуют со средствами интроспекции. Нередко возникает ситуация, когда inspect.getsource() и inspect.getsourcefile() успешно возвращают исходники для функций, но «ломаются» на классах, выбрасывая вводящее в заблуждение TypeError о «встроенном классе». И это при том, что класс явно определён в вашем динамически импортированном файле.

Как воспроизвести проблему

Возьмём минимальный пример из двух файлов, загружаемых через importlib. Исходный код функции извлекается, а у класса — нет.

loader.py

import inspect
import os
import importlib.util

base_dir = os.path.dirname(__file__)
src_spec = importlib.util.spec_from_file_location("lib_dyn", os.path.join(base_dir, "lib_dyn.py"))
loaded_mod = importlib.util.module_from_spec(src_spec)
src_spec.loader.exec_module(loaded_mod)

print(loaded_mod.bump(3))
print(loaded_mod.Counter().plus(3))

print("module source file:", inspect.getsourcefile(loaded_mod))
for item in ["bump", "Counter"]:
    ref = getattr(loaded_mod, item)
    print(f"{item} source: {inspect.getsourcefile(ref)}")
    print(inspect.getsource(ref))

lib_dyn.py

def bump(n):
    return n + 1

class Counter(object):
    def plus(self, n):
        return n + 1

При запуске исходный код функции печатается, а попытка получить исходник класса приводит к TypeError с сообщением, что это встроенный класс.

Что на самом деле происходит

Такое поведение связано с тем, как inspect восстанавливает путь от класса к модулю, где он определён. Логика поиска файла с исходниками для классов выглядит так:

if isclass(object):
    if hasattr(object, '__module__'):
        module = sys.modules.get(object.__module__)
        if getattr(module, '__file__', None):
            return module.__file__
        if object.__module__ == '__main__':
            raise OSError('source code not available')
    raise TypeError('{!r} is a built-in class'.format(object))

У классов в object.__module__ хранится лишь строка с именем модуля. Чтобы превратить её в объект модуля, inspect ищет его в sys.modules. Если там модуля нет, ветка кода выше делает вывод, что класс «встроенный», и выбрасывает знакомый вам TypeError. Иными словами, при динамической загрузке модуль не был зарегистрирован в sys.modules, а у inspect нет другого места, откуда его восстановить. Альтернативного API в inspect, позволяющего явно передать модуль, тут нет; в такой ситуации inspect.getmodule(loaded_mod.Counter) возвращает None, что подчёркивает зависимость от sys.modules.

Я что-то упускаю на этапе импорта, что должно подсказать Python, где взять исходники для классов?

Да. В рецепте из документации importlib по импорту исходного файла по пути явно показано обновление sys.modules, потому что exec_module этого за вас не делает.

Исправление

Зарегистрируйте динамически загруженный модуль в sys.modules под тем именем, которое вы задали в спецификации. После этого inspect сможет отследить класс до файла, где он объявлен.

Небольшой вспомогательный хелпер повторяет рецепт из importlib:

import importlib.util
import sys

def load_from_path(mod_name, file_path):
    sp = importlib.util.spec_from_file_location(mod_name, file_path)
    mod = importlib.util.module_from_spec(sp)
    sys.modules[mod_name] = mod
    sp.loader.exec_module(mod)
    return mod

Применим к предыдущему примеру:

loader_fixed.py

import inspect
import os
from importlib.util import spec_from_file_location, module_from_spec
import sys

root = os.path.dirname(__file__)
sp = spec_from_file_location("lib_dyn", os.path.join(root, "lib_dyn.py"))
modx = module_from_spec(sp)
sys.modules["lib_dyn"] = modx
sp.loader.exec_module(modx)

print(modx.bump(3))
print(modx.Counter().plus(3))

print("module source file:", inspect.getsourcefile(modx))
for name in ["bump", "Counter"]:
    obj = getattr(modx, name)
    print(f"{name} source: {inspect.getsourcefile(obj)}")
    print(inspect.getsource(obj))

Этого изменения достаточно, чтобы inspect без ошибок возвращал исходник класса. Если вас беспокоят конфликты имён в sys.modules, выберите уникальное имя модуля — оно полностью под вашим контролем при создании спецификации.

Почему это важно

Интроспекция опирается на глобальный реестр системы импорта. Если динамически загруженный модуль не виден в sys.modules, инструменты вроде inspect не смогут надёжно сопоставить классы с файлами. Итоговый TypeError звучит так, будто класс встроенный, что сбивает с толку; даже ветка с OSError «source code not available» выглядела бы ожидаемее. Регистрация динамически импортируемых модулей делает поведение inspect для функций и классов единообразным.

Выводы

Загружая модули по пути через importlib, обязательно внесите модуль в sys.modules до его исполнения. Это соответствует документации и позволяет inspect.getsource() и inspect.getsourcefile() корректно работать как для функций, так и для классов. Чтобы избежать коллизий, задавайте отличающееся имя модуля при создании спецификации. Помните: inspect восстанавливает определяющий модуль по object.__module__, а поиск происходит в sys.modules.