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. Если один и тот же исходный файл загружается дважды под разными именами, по сути вы получаете два разных класса с одинаковым кодом. Это ломает сравнения, маскирует тонкие ошибки и делает поведение зависимым от способа запуска программы. Единый путь импорта модуля гарантирует, что типы остаются идентичными и взаимодействие между модулями предсказуемо.

Выводы

Файлы с классами и переиспользуемой логикой следует воспринимать как импортируемые модули, а не как точки входа. Обеспечьте их единовременный импорт под стабильным именем и запускайте исполняемую часть только после импорта. Это предотвращает повторную загрузку модулей, исключает несоответствие тождественности классов и делает проверки равенства надёжными.