Введение
Статья 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++.