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

Сигналы и слоты в Python: Signal, Slot, connect — отличия от C++ синтаксиса

Введение

Статья 500 уже использовала button.clicked.connect(self.on_button_clicked) без подробного разбора — в этой статье разберём механизм сигналов и слотов в PySide6 системно, включая объявление собственных сигналов и явные отличия синтаксиса от привычного по основному циклу статей C++ варианта.

Концепция

Встроенные сигналы Qt-классов (clicked у QPushButton) доступны в Python напрямую как атрибуты объекта, и подключение обработчика выполняется через метод .connect(), принимающий любой вызываемый объект Python (функцию, метод, lambda) — это значительно более гибко по сравнению с C++ синтаксисом connect(), требующим точного указания типа сигнатуры через указатели на функции-члены или явный синтаксис нового стиля connect. Собственные, пользовательские сигналы объявляются через класс Signal (импортируемый из PySide6.QtCore) как атрибут класса, унаследованного от QObject — декоратор @Slot (опциональный, но рекомендуемый для производительности и явности) помечает метод как Qt-слот, что особенно важно для совместимости с механизмами Qt, ожидающими именно слот (а не произвольный Python callable) в некоторых конкретных контекстах.

Пример кода: Сигналы и слоты в Python

# Объявление и использование пользовательского сигнала
from PySide6.QtCore import QObject, Signal, Slot

class TemperatureSensor(QObject):
    temperatureChanged = Signal(float)  # объявление сигнала с одним параметром типа float

    def __init__(self):
        super().__init__()
        self._temperature = 0.0

    def setTemperature(self, value: float):
        if value != self._temperature:
            self._temperature = value
            self.temperatureChanged.emit(value)  # генерация сигнала — emit(), а не явный синтаксис C++


class TemperatureDisplay(QObject):
    @Slot(float)  # декоратор @Slot не обязателен для работы, но рекомендован для ясности и оптимизации
    def onTemperatureChanged(self, value: float):
        print(f"Новая температура: {value}°C")


sensor = TemperatureSensor()
display = TemperatureDisplay()
sensor.temperatureChanged.connect(display.onTemperatureChanged)
sensor.setTemperature(23.5)  # выведет: "Новая температура: 23.5°C"
# connect() с lambda — гибкость, недоступная так же просто в C++ без явного создания отдельной функции
from PySide6.QtWidgets import QPushButton

button = QPushButton("Удвоить")
counter = {"value": 1}

button.clicked.connect(lambda: print(f"Текущее значение: {counter['value'] * 2}"))
# Lambda как обработчик сигнала — идиоматичный, частый паттерн в Python-коде PySide6,
# особенно для простых, коротких реакций, не оправдывающих создание отдельного именованного метода
# Сигнал с несколькими перегрузками (overload) — менее распространённый, но поддерживаемый случай
from PySide6.QtCore import Signal

class MultiSignalExample(QObject):
    valueChanged = Signal(int)         # первая "перегрузка" — для int
    valueChangedStr = Signal(str)       # для строк используется ОТДЕЛЬНОЕ имя сигнала,
                                          # а не настоящая перегрузка как в C++ Qt с одинаковым именем сигнала

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

Signal(float) показывает объявление сигнала как атрибута класса с указанием типа передаваемого параметра — в отличие от C++, где сигнал объявляется через макрос signals: внутри класса с явным указанием в заголовочном файле, в Python это просто атрибут класса, и вызов self.temperatureChanged.emit(value) генерирует сигнал, аналогично макросу emit в C++ Qt (хотя в Python emit — обычный метод, а не специальное ключевое слово). @Slot(float) показывает необязательный, но рекомендуемый декоратор, явно фиксирующий тип параметра слота — без этого декоратора метод всё равно может служить обработчиком сигнала (благодаря динамической природе Python), но явное использование @Slot улучшает производительность подключения и делает код более явным и проверяемым статическими анализаторами. Использование lambda в button.clicked.connect(lambda: ...) показывает практичную особенность Python-кода — короткие, одноразовые обработчики часто пишутся как анонимные функции непосредственно в месте подключения, без необходимости заранее объявлять отдельный именованный метод исключительно для этой простой цели.

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

  • Захват изменяемых переменных в lambda-обработчике без понимания особенностей замыканий Python, особенно в циклах (классическая ловушка Python, не специфичная для PySide6, но особенно часто проявляющаяся именно при подключении нескольких однотипных сигналов в цикле) — lambda внутри цикла захватывает переменную цикла по ссылке, а не по значению на момент создания lambda, и без явного, дополнительного связывания (через параметр со значением по умолчанию) все созданные в цикле lambda-обработчики при срабатывании будут использовать одно и то же, финальное значение переменной цикла, а не то значение, которое было актуально в момент создания каждой конкретной lambda.
  • Создание сильной ссылки через connect(), препятствующей сборке мусора объекта-получателя сигнала, если этот объект не имеет других, явных ссылок на себя — в зависимости от способа подключения (особенно для bound-методов объекта, не сохранённого в долгоживущей переменной) подключение через connect() может неявно продлевать время жизни объекта-получателя дольше, чем ожидал разработчик, что в редких, специфичных случаях может привести к неочевидным утечкам памяти, требующим явного disconnect() для разрыва такой связи.
  • Игнорирование декоратора @Slot для методов, используемых как обработчики сигналов, в производительности критичных участках кода с очень частым срабатыванием сигналов — без явного @Slot-декоратора PySide6 должен динамически определять подходящий способ вызова метода при каждом срабатывании сигнала, что несколько медленнее явно объявленного, заранее известного по типу параметров слота, и для кода с действительно высокой частотой срабатывания сигналов (тысячи раз в секунду) эта разница в производительности может стать заметной.
  • Попытка прямого переноса C++ синтаксиса перегрузки сигналов с одинаковым именем, но разными типами параметров — в отличие от C++ Qt, где один и тот же сигнал может иметь несколько перегруженных версий с одинаковым именем, но разной сигнатурой параметров, в Python для разных типов параметров обычно используются отдельные, явно различающиеся по имени сигналы (как показано в примере valueChanged/valueChangedStr), и попытка применить точно такой же подход к перегрузке, как в C++, потребует более сложного, специфичного синтаксиса (Signal(int, name='valueChanged') для явного разрешения неоднозначности), который менее интуитивен, чем естественная перегрузка C++.