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.