2025, Dec 29 06:02

Почему PyQt5-стиль enum «работает» в PyQt6 и как это исправить

Почему в PyQt6 старый доступ к enum вроде QMessageBox.Ok иногда «работает»: роль QtPy/qwt, риски патчинга и как правильно использовать пространства имён.

Почему код в стиле PyQt5 для QMessageBox, кажется, работает под PyQt6? Если вы замечали, что устаревший доступ к перечислениям вроде QMessageBox.Ok и QMessageBox.Information ведёт себя так, будто ничего не изменилось, на ум приходит объяснение про скрытый фолбэк в PyQt6. Его нет. Причина в другом.

Постановка задачи

Два Python‑приложения запускаются в одном и том же виртуальном окружении (PyQt6.5, Python 3.11). Одно использует новые пространства имён перечислений PyQt6 — и работает как ожидается. Другое обращается к старым, «пятёрочным» enum’ам, но тоже запускается, причём PyQt5 не установлен. Это удивляет, ведь PyQt6 обычно требует квалифицированные пространства имён для перечислений.

Вот пример использования в стиле PyQt6:

alertDlg = QMessageBox(parent=self)
alertDlg.setText('WHOOPS - Must have at least one (1) joystick attached')
alertDlg.setStandardButtons(QMessageBox.StandardButton.Ok)
alertDlg.setIcon(QMessageBox.Icon.Warning)
alertDlg.exec()

А вот устаревший вариант в стиле PyQt5, который тоже, казалось, работал:

noticeBox = QMessageBox(parent=self)
noticeBox.setText('Whoops - Upload destination not set.\n' +
                  'Save animation file to implicitly set or\n' +
                  'Edit metadata to explicitly set.')
noticeBox.setStandardButtons(QMessageBox.Ok)
noticeBox.setIcon(QMessageBox.Information)
resultFlag = noticeBox.exec_()

Дополнительно в начале файла есть блок импортов: сначала пробует PyQt5, затем переключается на PyQt6 и завершает работу, если ни один из вариантов недоступен:

# Pick local Qt binding: try PyQt5, else PyQt6, else abort
qt_binding = None
try:
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    from PyQt5 import QtMultimedia as media
    qt_binding = 5
except:
    try:
        from PyQt6.QtCore import *
        from PyQt6.QtGui import *
        from PyQt6.QtWidgets import *
        from PyQt6 import QtMultimedia as media
        qt_binding = 6
    except:
        sys.stderr.write('WHOOPS - Unable to find PyQt5 or PyQt6 - Quitting\n')
        exit(10)

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

Старый код работает потому, что другая зависимость — qwt — подтягивает под капотом абстракционный слой QtPy. QtPy и похожие совместимые прослойки существуют, чтобы сгладить различия API между биндингами и версиями Qt. Одно из самых заметных изменений в PyQt6 — каждый Qt‑enum должен жить в своём пространстве имён. Это улучшает то, как Python видит перечисления (теперь это настоящие Python‑enum’ы) и согласуется с Qt, но одновременно означает, что прежний плоский доступ к атрибутам, вроде QMessageBox.Ok, по умолчанию больше недоступен.

Чтобы скрыть эти различия, слой вроде QtPy программно создаёт или навешивает атрибуты на модули и классы во время импорта. Как только модуль импортирован в Python, он остаётся в памяти, и любые атрибуты, которые совместимая прослойка дописывает, становятся видны глобально в процессе. Если QtPy добавляет атрибут Ok в QMessageBox, то в вашей программе появляется QMessageBox.Ok. Отсюда и расхождение: приложение, которое импортирует qwt (а он зависит от QtPy), как бы «наследует» старый паттерн доступа к enum’ам; приложение без этой зависимости падает, если не использовать новый синтаксис с пространствами имён.

По умолчанию PyQt6 выбросит AttributeError, если обратиться к QMessageBox.Ok вместо QMessageBox.StandardButton.Ok. Никакого встроенного фолбэка нет. Видимый успех объясняется лишь тем, что абстракционный слой пропатчил атрибуты модуля. Некоторые биндинги ведут себя иначе: PySide6 поддерживает новый подход с Python‑enum’ами, но дополнительно содержит внутренний «режим снисходительности», поэтому QMessageBox.Ok там всё ещё работает. Однако такая поблажка может исчезнуть в будущем.

Имейте в виду: создавать атрибуты на внешних объектах — не всегда хорошая идея. Qt старается избегать конфликтов имён, но при масштабе API абсолютной гарантии нет. Есть и производственные компромиссы: такие прослойки либо массово патчат всё на старте — замедляя запуск, — либо делают это по требованию, что добавляет накладные расходы на поиск и увеличивает число движущихся частей, которые могут сломаться в краевых сценариях. Тем не менее, для кодовых баз, которым нужно работать с несколькими версиями Qt или разными биндингами, это заметно снижает трение.

Решение: явно использовать пространства имён перечислений Qt6

Надёжный путь — всегда применять полностью квалифицированные имена enum’ов, как того требует Qt6. Это избавляет от скрытой привязки к совместимой прослойке и гарантирует одинаковое поведение кода вне зависимости от наличия стороннего абстракционного слоя.

Вот исправленный вариант устаревшего фрагмента, переписанный под подход Qt6:

dialog = QMessageBox(parent=self)
dialog.setText('Whoops - Upload destination not set.\n' +
               'Save animation file to implicitly set or\n' +
               'Edit metadata to explicitly set.')
dialog.setStandardButtons(QMessageBox.StandardButton.Ok)
dialog.setIcon(QMessageBox.Icon.Information)
choice = dialog.exec()

Если вашему проекту также нужно работать со старыми биндингами, совместимая прослойка может пригодиться, но лучше писать прикладной код уже с новыми пространствами имён перечислений, а шиму отдать только то, что действительно необходимо. Так вы не окажетесь зависимыми от побочных эффектов вроде «пропатченных» атрибутов, чтобы диалоги вообще открывались.

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

Понимание этого поведения уберегает от случайной зависимости от неявного патчинга. А ещё защищает ваш код от багов, зависящих от окружения. Если один модуль тихо переписывает атрибуты у класса Qt, поведение может меняться из‑за порядка импортов, наличия тех или иных пакетов или даже небольшой рефакторинг, убравший, казалось бы, несвязанный импорт. Такая хрупкость в продакшене ни к чему.

Есть и эксплуатационный аспект: совместимые слои, которые интроспектируют модули Qt и навешивают тысячи enum’ов, способны увеличивать время холодного старта или вносить тонкие регрессии. Если важен быстрый запуск, стоит подумать, хотите ли вы, чтобы всё это патчинг‑поведение происходило за кулисами. Если же для вас приоритет — читаемость и единый API в разных средах, компромисс может быть оправдан, — при условии, что вы осознаёте, на что соглашаетесь.

Практические выводы

Не рассчитывайте, что PyQt6 примет enum’ы в стиле PyQt5. Если видите, что старый доступ к перечислениям «работает» в среде только с PyQt6, поищите абстракционный слой вроде QtPy, который мог зайти в процесс опосредованно через зависимость наподобие qwt. В коде под Qt6 отдавайте предпочтение полным пространствам имён для перечислений, чтобы избежать сюрпризов. Если всё же используете совместимую прослойку, учитывайте последствия программного патча атрибутов для производительности и сопровождаемости. А в конструкциях с импорт‑фолбэком, как выше, рассмотрите более точную обработку исключений в вашем коде, чтобы упростить и обезопасить отладку — даже если логика фолбэка останется прежней.

Короче, самый безопасный путь вперёд — обновить обращения к QMessageBox и прочим enum’ам до формы с пространствами имён Qt6 и относиться к совместимым шимам как к необязательным помощникам, а не скрытым основаниям вашего кода.