Различные статьи по Qt и c++

Различия синтаксиса Python и C++ при работе с Qt API: свойства, перечисления, типы

Введение

Статьи 500-501 уже коснулись отдельных синтаксических различий — в этой статье систематизируем основные расхождения между Python и C++ синтаксисом при работе с одним и тем же, по сути, Qt API: свойства (Q_PROPERTY), перечисления, и общие соответствия типов между языками.

Концепция

Q_PROPERTY из C++ (основной цикл статей, статья 490 QML-цикла) в Python выражается через декоратор @Property (из PySide6.QtCore), применяемый к обычным Python-методам в стиле getter/setter, что синтаксически ближе к Python-идиоме @property стандартной библиотеки, но с дополнительными возможностями интеграции с системой сигналов Qt (notify-параметр). Перечисления Qt (Qt.AlignmentFlag, Qt.Orientation) доступны в Python через атрибуты соответствующих классов, с некоторыми отличиями в точности необходимого пути доступа между версиями PySide (статья 499 уже упоминала эту тонкость). Соответствие типов между Python и C++ в большинстве случаев происходит автоматически и прозрачно (Python int/float/str/bool/list преобр��зуются в соответствующие C++ типы Qt API и обратно), но для некоторых специфичных Qt-типов (например, QVariant) точное поведение преобразования стоит понимать явно.

Пример кода: Различия синтаксиса Python

# @Property — Python-аналог Q_PROPERTY из C++ (статья 490 QML-цикла)
from PySide6.QtCore import QObject, Property, Signal

class UserProfile(QObject):
    nameChanged = Signal(str)

    def __init__(self):
        super().__init__()
        self._name = ""

    def getName(self) -> str:
        return self._name

    def setName(self, value: str):
        if self._name != value:
            self._name = value
            self.nameChanged.emit(value)

    name = Property(str, getName, setName, notify=nameChanged)
    # ^ "name" теперь доступно для использования в QML точно так же, как Q_PROPERTY в C++
# Работа с перечислениями Qt — синтаксис может незначительно отличаться между версиями PySide6
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QLabel

label = QLabel("Текст по центру")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)  # явная, полная форма — рекомендуемый, надёжный вариант

# Комбинирование флагов через побитовое ИЛИ — аналогично C++ синтаксису
label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
# Соответствие типов Python <-> Qt — большинство случаев прозрачны
from PySide6.QtCore import QSize, QPoint

size = QSize(100, 50)         # явное создание Qt-типа из Python — синтаксис похож на конструктор C++
point = QPoint(10, 20)

# Список Python автоматически преобразуется в QStringList/QVariantList там, где это ожидается API
items = ["Первый", "Второй", "Третий"]  # обычный Python list, используемый напрямую с Qt API

Пояснения к коду

Property(str, getName, setName, notify=nameChanged) показывает Python-эквивалент Q_PROPERTY — параметры getName/setName соответствуют READ/WRITE из C++ макроса, а notify=nameChanged — параметру NOTIFY, и итоговое поведение (доступность этого свойства из QML с поддержкой автоматического обновления привязок при изменении, статья 481/490 QML-цикла) полностью идентично C++ версии, несмотря на иной, более питонический синтаксис объявления. Qt.AlignmentFlag.AlignCenter показывает рекомендуемую, полную форму доступа к значению перечисления — явное указание имени самого перечисления (AlignmentFlag) перед конкретным значением (AlignCenter) повышает совместимость кода между разными версиями PySide6 и явно указывает на конкретное перечисление, к которому относится значение, в отличие от более короткой, но менее однозначной формы. Прямое использование Python list для items показывает прозрачность преобразования базовых типов — для большинства типичных случаев Python-разработчик может работать с обычными, идиоматичными Python-структурами данных, не задумываясь о деталях преобразования в соответствующие Qt/C++ типы, происходящего автоматически на границе вызова Qt API.

Подводные камни

  • Использование сокращённой, неполной формы доступа к значениям перечисления (Qt.AlignCenter вместо Qt.AlignmentFlag.AlignCenter), которая может работать в одной версии PySide6, но не гарантированно работать в другой (статья 499) — для написания кода, устойчивого к обновлению используемой версии PySide6, рекомендуется использовать именно полную, явную форму доступа к перечислениям везде, где это возможно, даже если сокращённая форма формально работает в текущей, используемой версии библиотеки.
  • Путаница между Python-нативным @property стандартной библиотеки и Qt-специфичным @Property из PySide6 — хотя они синтаксически похожи и решают концептуально похожую задачу (управляемый доступ к атрибуту через getter/setter), только @Property из PySide6 даёт интеграцию с системой Qt-свойств, необходимую для работы с QML и системой NOTIFY-сигналов, и обычный Python @property не предоставляет этой интеграции, несмотря на внешнее синтаксическое сходство, способное создать ложное ожидание идентичного поведения.
  • Неявное предположение, что любой Python-объект автоматически и без проблем преобразуется в любой ожидаемый Qt-тип параметра — хотя многие базовые типы преобразуются прозрачно (как показано с list), для более специфичных, сложных Qt-типов (QVariant, пользовательские типы C++, экспонированные в Qt API) могут потребоваться явные преобразования или специфичные обёртки, и слепое предположение об автоматической, универсальной совместимости любого Python-объекта с любым Qt API без проверки документации может привести к неочевидным ошибкам типов во время выполнения.
  • Игнорирование различий в обработке None (Python) и nullptr/невалидных Qt-объектов (C++) — Python None и Qt-специфичные представления «отсутствия» значения (невалидный указатель, пустой QVariant) не всегда взаимно однозначно соответствуют друг другу во всех контекстах Qt API, и код, написанный с предположением о полной эквивалентности этих двух концепций отсутствия значения, может вести себя не так, как ожидалось бы в эквивалентном C++ коде с явным nullptr.