Signal-Slot-Mechanismus von PyQt

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
lokiak
User
Beiträge: 7
Registriert: Samstag 18. Juni 2022, 20:58

Hallo,

ich versuche gerade den Signal-Slot-Mechanismus von PyQt zu verstehen. Dazu habe ich mir zuerst die Qt-Dokumentation angeschaut. Die meine ich halbwegs zu verstehen. Signals und Slots sind hier beides public-Funktionen, wobei die Signal-Funktion nur deklariert und nicht implementiert wird. Die Arbeit übernimmt das MOS. Korrekt?

In PyQt gibt es die Klasse QtCore.pyqtSignal. Zumindest glaube ich, dass es sich hierbei um eine Klasse handelt, finden kann ich sie nicht unter https://docs.huihoo.com/pyqt/PyQt5/QtCore.html.
Um pyqtSignal nutzen zu können lege ich eine Klassenvariable (class attribute) an. An dieser Stelle handelt es sich noch um ein unbound-signal. PyQt übernimmt dann die Bindung an eine Instanz (?). Warum wird an dieser Stelle die unbound-bound-Methode verwendet? Mir ist der Nutzen nicht klar.

Vielen Dank und Viele Grüße
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@lokiak: Signale sind keine Funktionen. Also zumindest nicht auf der Python-Seite, wo sie ein Datentyp/eine Klasse sind (oder mehrere). Bei C++ würde ich sagen ist das was ”eigenes”, denn da wird die Sprache um das Signal/Slot-Konzept mit den Schlüssworten ``signals`` und ``emit`` erweitert.

Was `pyqtSignal()` letztendlich ist, Klasse oder Funktion, ist ein Implementierungsdetail. Benutzt wird es wie eine Funktion, weshalb es wahrscheinlich nicht bei den Klassen von `QtCore` gelistet ist.

Wie sollte es denn ohne unbound/bound funktionieren? Man definiert/deklariert das Signal für die Klasse, dass heisst jedes Exemplar das erstellt wird, soll das Signal senden können. Und man muss die Signale von der Klasse abfragen können, weil Qt das so vorsieht. Die Exemplare müssen dann aber jeweils ein eigenes, an das Exemplar gebundene Signal haben, denn man verbindet ja die Signale von individuellen Objekten mit Slots, und wenn man ein Signal sendet, dann von einem konkreten Objekt und nicht für alle Objekte der jeweiligen Klasse.

Das ist weniger eine Frage des Nutzens, sondern das man das so machen muss wenn man das Signal/Slot-Konzept von Qt in Python abbilden will.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du meinst den MOC, nicht MOS. Der erzeugt die notwendigen Meta-Objekt-Implementierungen und die Methodenrümpfe für die Signale. Slots können auch private sein, ist aber letztlich wurscht, weil sei via dem Meta-Objekt immer aufgerufen werden können.

Und genau dieses Meta-Objekt ist auch der Grund, warum das erstmal Klassenattribute sind. Denn damit die Python-Klassen und daraus erzeugte Objekte mit C++ interagieren können, braucht es C++-Meta-Objekte, und ihre QObject-abgeleitete C++-Klasse, “auf Vorrat”. Und die modifiziert PyQt dann und legt die gewünschten Signale und Slots an. Es gibt davon auch nur eine begrenzte Anzahl, aber normalerweise reicht das aus. Und das sind ja pro Klasse(!) ein Objekt. Nicht pro Instanz. Daher sind das auch Klassen-Attribute. Zur Laufzeit wird dann beim erzeugen eines Objektes das dazugehörige C++-QObject erzeugt, damit die ganze Maschinerie funktioniert.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@__deets__: Ich vermute mit „MOS“ war „Meta-Object System“ gemeint. Unter der Überschrift gibt es einen kurzen Überblick in der Qt-Dokumentation.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
lokiak
User
Beiträge: 7
Registriert: Samstag 18. Juni 2022, 20:58

@blackjack:
Signale sind keine Funktionen. Also zumindest nicht auf der Python-Seite, wo sie ein Datentyp/eine Klasse sind (oder mehrere)
Was `pyqtSignal()` letztendlich ist, Klasse oder Funktion, ist ein Implementierungsdetail. Benutzt wird es wie eine Funktion, weshalb es wahrscheinlich nicht bei den Klassen von `QtCore` gelistet ist.
Jetzt komme ich durcheinander. 'pyqtSignal' kann beides sein? Also mit meinen beschränkten Programmierkenntnissen hätte ich gedacht, dass man entweder-oder hat. Klasse oder Funktion. Und in diesem Fall hätte ich gedacht, dass 'pyqtSignal' eine Klasse ist, mit den Klassenfunktionen 'emit()' und 'connect'. Anders kann ich mir das im folgenden Beispielcode nicht erklären.

Code: Alles auswählen

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import pyqtSignal, Qobject

class Communicate(QObject):
        closeApp = pyqtSignal()

class Example(QMainWindow):
        def __init__(self):
        super().__init__()
        self.initUI()
        
         def initUI(self):
          self.c = Communicate()
          self.c.closeApp.connect(self.close)
          self.setGeometry(300, 300, 300, 150)
          self.setWindowTitle('Mouse events') 
          self.show()
        
        def mousePressEvent(self, event):
        self.c.closeApp.emit()
        
if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())
Aber, wie gesagt, meine Programmierkenntnisse sind begrenzt. Arbeite aber daran ;)
Wie sollte es denn ohne unbound/bound funktionieren? Man definiert/deklariert das Signal für die Klasse, dass heisst jedes Exemplar das erstellt wird, soll das Signal senden können. Und man muss die Signale von der Klasse abfragen können, weil Qt das so vorsieht. Die Exemplare müssen dann aber jeweils ein eigenes, an das Exemplar gebundene Signal haben, denn man verbindet ja die Signale von individuellen Objekten mit Slots, und wenn man ein Signal sendet, dann von einem konkreten Objekt und nicht für alle Objekte der jeweiligen Klasse.
Also so wie Du das hier erklärst klingt das einfach nach einer Klassenvariable, also einfach nach einer Variablen, die, wenn eine Instanz dieser Klasse erzeugt wird, eben dieser einen Instanz gehört.

VG
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@lokiak: `pyqtSignal()` kann nicht *gleichzeitig* eine Funktion und eine Klasse sein, aber aus der Dokumentation geht nicht hervor, dass es eine Klasse ist. Dort wird es wie eine Funktion beschrieben, und es könnte auch problemlos eine sein.

Tatsächlich ist es wohl eine Klasse, aber die hat weder eine `connect()`- noch eine `emit()`-Methode. Denn auf diese Objekte greift man auf Exemplaren ja gar nicht zu. Dort hat jedes Exemplar noch mal ein eigenes Objekt von einem anderen Typ: `pyqtBoundSignal`. Die werden beim Initialisieren erstellt.

Signale als Klassenattribute würden nicht funktionieren. `QPushButton` haben ein `clicked`-Signal. Das darf es nicht nur einmal geben, denn wenn man zwei Buttons erzeugt, will man ja in der Regel, dass jeder davon sein eigenes `clicked`-Signal hat, das man unabhängig an unterschiedliche Slots binden kann. Das geht nicht wenn es auf der Klasse nur *ein* Signal-Objekt gäbe. Auf der Klasse muss aber definiert werden, welche Signale diese Klasse hat. Weil das Meta-Object-Modell von Qt diese Informationen über die Klasse verfügbar macht.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
lokiak
User
Beiträge: 7
Registriert: Samstag 18. Juni 2022, 20:58

@blackjack:
Tatsächlich ist es wohl eine Klasse, aber die hat weder eine `connect()`- noch eine `emit()`-Methode. Denn auf diese Objekte greift man auf Exemplaren ja gar nicht zu. Dort hat jedes Exemplar noch mal ein eigenes Objekt von einem anderen Typ: `pyqtBoundSignal`. Die werden beim Initialisieren erstellt.
Der Grund für mich anzunehmen, dass es sich bei 'pyqtSignal' um eine Klasse mit den 'connect()' und 'emit()' Funktionen handelt, war die Punktnotation im gezeigt Beispielcode. So kenn ich es mit meinen limitierten Programmierkenntnissen: man schreibt eine Klasse mit Funktionen, erzeugt eine Instanz und greift über 'Namen_der_Instanz.Funktionsnamen" auf die Funktion zu. Aber das scheint hier anders zu sein.
Mit Exemplare meinst Du Instanzen? Sorry, ich komme nicht mit.
Signale als Klassenattribute würden nicht funktionieren.
Auf https://www.riverbankcomputing.com/stat ... slots.html habe ich gelesen >> A signal (specifically an unbound signal) is a class attribute. << Deshalb sprach ich von Klassenattributen.

Noch einmal zurück zu Deiner 1sten Antwort:
Bei C++ würde ich sagen ist das was ”eigenes”, denn da wird die Sprache um das Signal/Slot-Konzept mit den Schlüssworten ``signals`` und ``emit`` erweitert.
Auf https://doc.qt.io/qt-5/signalsandslots.html fand ich
Signals are public access functions and can be emitted from anywhere, but we recommend to only emit them from the class that defines the signal and its subclasses.

Sind es nicht also schlussendlich einfache Funktionen in C++?
Wie sollte es denn ohne unbound/bound funktionieren? Man definiert/deklariert das Signal für die Klasse, dass heisst jedes Exemplar das erstellt wird, soll das Signal senden können. Und man muss die Signale von der Klasse abfragen können, weil Qt das so vorsieht. Die Exemplare müssen dann aber jeweils ein eigenes, an das Exemplar gebundene Signal haben, denn man verbindet ja die Signale von individuellen Objekten mit Slots, und wenn man ein Signal sendet, dann von einem konkreten Objekt und nicht für alle Objekte der jeweiligen Klasse.
Meine naive Idee (wie es funktionieren könnte) bisher: es gibt eine Klasse 'pyqtSignal' mit einer Funktion, z.B. emit(), die bei der Instanzbildung über 'self' eine Referenz auf sich selbst erhält. Die Instanz hat ja eine eigene Id (Speicheradresse). Mit Bezug auf die Id könnte die Rückgabe der emit-Funktion so etwas sein wie 'Id_der_Instanz_Signal'. Dann wäre das Signal eindeutig mit der Instanz verbunden. So in etwa, dachte ich bislang, würde das ablaufen. Aber es scheint komplexer zu sein, und ich verstehe offensichtlich noch nicht ansatzweise, wie Python über PyQt und Qt zusammen mit dem MOC funktionieren. Da werde ich mich wohl noch belesen müssen (für gute Quellen bin ich dankbar).

Vielen Dank auch an deets, Du hast es mir ebenfalls versucht zu erklären!

VG
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich vermute mal eher, dass hier das descriptor Protokoll zum Einsatz kommt, um aus der Klassenvariable ein an die konkrete Instanz gebundenes Signal Objekt zu machen.

Aber warum brauchst du diesen Detailgrad? Die konkrete Implementierung kann man im Code sehen, aber die ist doch für Nutzung egal.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@lokiak: Also erstens muss `pyqtSignal()` keine Klasse sein, nur weil man auf dem Ergebnis mittels Punktoperator auf Attribute zugreifen kann. Das geht ja auch wenn das eine Funktion (oder Methode) ist, die ein entsprechendes Objekt als Ergebnis liefert.

Nun ist `pyqtSignal` eine Klasse, aber die muss keine `emit()`- oder `connect()`-Methode haben — hat sie auch nicht — weil das `pyqtSignal`-Objekt ein *Klassenattribut* ist, `emit()` und `connect()` aber nicht auf diesem Klassenattribut, sondern auf einem gleichnamigen *Instanzattribut* aufgerufen werden, das einen anderen Typ als `pyqtSignal` hat. Nämlich `pyqtBoundSignal`:

Code: Alles auswählen

In [156]: class A(PyQt5.QtCore.QObject): 
     ...:     a_signal = PyQt5.QtCore.pyqtSignal() 
     ...:                                                                       

In [157]: A.a_signal                                                            
Out[157]: <unbound PYQT_SIGNAL a_signal()>

In [158]: A.a_signal.__class__                                                  
Out[158]: PyQt5.QtCore.pyqtSignal

In [159]: A.a_signal.emit                                                       
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-159-ea3197a3e275> in <module>
----> 1 A.a_signal.emit

AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'emit'

In [160]: a = A()                                                               

In [161]: a.a_signal                                                            
Out[161]: <bound PYQT_SIGNAL a_signal of A object at 0x7f9902e9bee8>

In [162]: a.a_signal.__class__                                                  
Out[162]: PyQt5.QtCore.pyqtBoundSignal

In [163]: a.a_signal.emit()

In [164]:
Deine Idee funktioniert so nicht weil das Exemplar von `pyqtSignal` an die *Klasse* gebunden wird. Woher sollte eine `emit()`-Methode *dort* wissen, für welches konkrete Objekt von dem Typ der Klasse sie aufgerufen wurde? Genau dafür gibt es dann ein `pyqtBoundSignal`-Objekt auf jedem einzlenen Exemplar, das aus der Klasse mit dem `pyqtSignal` erstellt wird.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und genau diese „Wandlung“ von unbound zu bound erledigt das descriptor-Protokoll, siehe https://github.com/baoboa/pyqt5/blob/11 ... l.cpp#L274
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wobei das, als ergänzende Anmerkung, auch wieder nur ein Implementierungsdetail ist. Das hätte man auch in `__init__()` (oder `__new__()`) auf `QObject` regeln können, das dort ”normale” Attribute für Signale auf der Klasse erstellt werden.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
lokiak
User
Beiträge: 7
Registriert: Samstag 18. Juni 2022, 20:58

@blackjack:
Also erstens muss `pyqtSignal()` keine Klasse sein, nur weil man auf dem Ergebnis mittels Punktoperator auf Attribute zugreifen kann. Das geht ja auch wenn das eine Funktion (oder Methode) ist, die ein entsprechendes Objekt als Ergebnis liefert.
Kannst Du mir vielleicht ein ganz simples Beispiel nennen? Danke!
Deine Idee funktioniert so nicht weil das Exemplar von `pyqtSignal` an die *Klasse* gebunden wird.
Gebunden wird doch eine Instanz (=Exemplar?) von `pyqtSignal` (oder `pyqtBoundSignal`?) an die Instanz der von QObject abgeleiteten Klasse. Korrekt?
Woher sollte eine `emit()`-Methode *dort* wissen, für welches konkrete Objekt von dem Typ der Klasse sie aufgerufen wurde? Genau dafür gibt es dann ein `pyqtBoundSignal`-Objekt auf jedem einzlenen Exemplar, das aus der Klasse mit dem `pyqtSignal` erstellt wird.
Dynamisch, in der Art der factory design pattern hätte ich gedacht.
Und genau diese „Wandlung“ von unbound zu bound erledigt das descriptor-Protokoll,...
Danke, ich werde es mir ansehen.

VG
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Code: Alles auswählen

def pyqtSignal():
     return SomeClass()
schon ist pyqtSignal keine Klasse, aber der erzeugte wert ist eine Instanz, auf der man dann auch mit dem .-Operator arbeiten kann.

Die Instanz eine Klasse ist ein Objekt. Und daran wird pyQtSignal NICHT gebunden. Wie BJ zeigt. Sondern an die Klasse selbst,und durch __get__ *bei Bedarf* ein bound Signal beim Zugriff via einer Instanz.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@lokiak: Ein Beispiel für eine Funktion hat __deets__ ja bereits gezeigt. Klasse und Funktion sind in Python an dieser Stelle austauschbar, weil man eine Klasse auch als (Fabrik)Funktion sehen kann, die beim Aufruf ein Objekt vom entsprechenden Typ erzeugt. Sowohl Klasse als auch Funktion sind „callable“, und das ist hier für den Benutzer die einzige API die er braucht — etwas das man aufrufen kann und das ein passendes Objekt liefert.

Gebunden wird ein Objekt vom Typ `pyqtSignal` an die von QObject abgeleitete Klasse. Nicht an eine Instanz von dieser Klasse sondern an die Klasse selbst. Das ist in Python auch ein Objekt. Wie alles was man an einen Namen binden kann, in Python ein Objekt ist. Auch Module, Funktionen, und Methoden beispielsweise.

Das `pyqtBoundSignal` entsteht beim Zugriff auf das Attribut durch den Aufruf der `__get__()`-Methode vom `pyqtSignal`-Objekt durch das Descriptor-Protokoll. Mit dem hat man normalerweise sehr selten direkt selbst etwas zu tun, das ist aber wichtiger Bestandteil der ”Magie” für Klassen. Methoden funktionieren beispielsweise auch darüber das Funktionen eine `__get__()`-Methode besitzen, damit aus der Funktion auf dem Klassen-Objekt eine Methode wird, wenn man über ein Objekt vom Typ dieser Klasse zugreift.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten