2025, Dec 27 09:01
Классы из __main__ и импортируемого модуля: почему они разные и как это исправить в Python
Статья объясняет, почему в Python класс из __main__ и того же модуля считаются разными: импорт загружает файл дважды. Разбор и решение через точку входа.
Когда один и тот же класс как будто «приходит» из двух разных модулей, равенство начинает вести себя неожиданно. Это проявляется и с dataclass, и с обычными классами: вы сравниваете два экземпляра, которые должны совпадать, но этого не происходит, потому что их типы имеют разные полные имена. Первопричина не в dataclasses, а в том, как Python обращается с модулем-точкой входа по сравнению с импортируемым модулем.
Как воспроизвести проблему
Рассмотрим два файла. Первый служит точкой входа и одновременно объявляет класс. Второй импортирует первый и создаёт экземпляр.
модуль A:
import B
class RecordBox:
pass
if __name__ == '__main__':
first = RecordBox()
second = B.build_obj()
print(type(first))
print(type(second))
модуль B:
import A
def build_obj():
return A.RecordBox()
Запуск A.py выводит два разных источника того, что должно быть одним и тем же классом, например:
<class '__main__.RecordBox'>
<class 'A.RecordBox'>
Что происходит на самом деле
Суть проблемы в том, что A.py выполняется как точка входа и поэтому является модулем с именем
__main__, а не модулем с именемA.
Когда вы запускаете программу, выполняя A.py напрямую, Python загружает этот файл как модуль __main__. Позже, когда B импортирует A, Python ищет модуль с именем A. Поскольку выполняющаяся точка входа называется __main__, в кэше импорта он под A не обнаружится и будет загружен ещё раз как отдельный объект модуля. В итоге в памяти оказываются два разных объекта модуля: один привязан к __main__, другой — к A. Каждый из них определяет свой собственный класс RecordBox, и эти объекты классов различны. Экземпляры, созданные из разных объектов классов, никогда не считаются равными, а вывод type() показывает расхождение.
Эта ситуация близка к циклическому импорту и усиливает путаницу вокруг тождественности типов и сравнения. Это ожидаемое следствие механизма импорта, а не ошибка, связанная с dataclass.
Как это исправить
Решение — не запускать файл, в котором объявлен класс, как точку входа. Организуйте код так, чтобы модуль сначала импортировался, а уже затем вы вызывали исполняемую логику. Проще всего вынести код запуска в функцию и вызывать её после импорта модуля.
Обновлённый A.py:
import B
class RecordBox:
pass
def bootstrap():
x = RecordBox()
y = B.build_obj()
print(type(x))
print(type(y))
if __name__ == '__main__':
bootstrap()
Теперь импортируйте модуль и вызовите функцию, чтобы модуль везде имел имя A:
python -c "import A; A.bootstrap()"
В результате типы совпадают, как и ожидается:
<class 'A.RecordBox'>
<class 'A.RecordBox'>
На практике удобно иметь отдельный скрипт-точку входа, который импортирует ваш «библиотечный» модуль и вызывает его bootstrap-функцию, либо вынести класс в отдельный модуль, а логику запуска держать отдельно. Главное — гарантировать, что класс определяется единожды под одним именем модуля до того, как где-либо создаются его экземпляры.
Почему это важно
Тождественность класса лежит в основе проверок равенства, isinstance и любых правил сравнения dataclass. Если один и тот же исходный файл загружается дважды под разными именами, по сути вы получаете два разных класса с одинаковым кодом. Это ломает сравнения, маскирует тонкие ошибки и делает поведение зависимым от способа запуска программы. Единый путь импорта модуля гарантирует, что типы остаются идентичными и взаимодействие между модулями предсказуемо.
Выводы
Файлы с классами и переиспользуемой логикой следует воспринимать как импортируемые модули, а не как точки входа. Обеспечьте их единовременный импорт под стабильным именем и запускайте исполняемую часть только после импорта. Это предотвращает повторную загрузку модулей, исключает несоответствие тождественности классов и делает проверки равенства надёжными.