Das unbarmherzige pyqtSignal()

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Leute,

ich weiß, dass pyqtSignal() keine Schlüsselargumente (keyword arguments) zulässt - warum auch immer. In meinen ausführbaren Beispiel-Quelltext habe ich mal versucht zu simulieren, worauf ich hinaus möchte. Ich habe hier eine print_it()-Methode, die neben dem normalen Argument auch zwei Schlüsselargumente entgegen nimmt. Die Sache ist nun folgendes: an mehreren Stellen im Programm wird diese Methode benutzt. Sehen wir mal an dieser Stelle darüber hinweg, dass die Schlüsselargumente unsinnige Werte besitzen - es geht um das allgemeine Verständnis. Weil diese Methode an mehreren Stellen verwendet wird, möchte ich keine identischen Aufgaben programmiertechnisch doppelt und dreifach wiederholen. Ich habe gehofft, hierbei alles "zusammenzufassen". Aber leider habe ich keine Idee, wie ich das Problem lösen könnte.

Code: Alles auswählen

from sys import argv

from PyQt4.QtCore import Qt, pyqtSignal, QObject
 
from PyQt4.QtGui import QDialog, QApplication, QPushButton, \
     QFormLayout

class WorkClass(QObject):
    
    test_signal = pyqtSignal(object)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

    def run(self):
        self.test_signal.emit(self.test_signal.emit('normal argument', keyword_arg_second="Ok, second"))
            
class Form(QDialog):

    start_work_class_signal = pyqtSignal()
    
    def __init__(self, parent=None):
        QDialog.__init__(self, parent)
        
        self.init_ui()

    def init_ui(self):

       
        self.pushButton_pyqt_signal = QPushButton()
        self.pushButton_pyqt_signal.setText("pyqtSignal") 
        
        layout = QFormLayout()
        layout.addWidget(self.pushButton_pyqt_signal)
        
        self.setLayout(layout)
        self.setWindowTitle("Testing window")

        self.pushButton_pyqt_signal.clicked.connect(self.start_with_work)

    def print_it(self, argument, keyword_arg_first=None, keyword_arg_second=None):
        print "Do some with argument", argument
        print "Do some with first keyword:", keyword_arg_first
        print "Do some with second keyword", keyword_arg_second
        return
    
    def start_with_work(self):
        work_class = WorkClass()
        work_class.test_signal.connect(self.print_it)
        
        self.start_work_class_signal.connect(work_class.run)
        self.start_work_class_signal.emit()

app = QApplication(argv)
form = Form()
form.show()
app.exec_()
BlackJack

@Sophus: Das geht halt nicht. Die Qt-API ist kein Wunschkonzert sondern das was mit C++ an der Stelle sinnvoll möglich ist. Grundsätzlich sollten die Signale die man mit Python definiert auch mit C++-Slots verwendbar sein. In dem Sinne würde ich auch nicht `object` als Datentyp verwenden wenn man tatsächlich Zeichenketten verwenden will, denn mit `object` kann man das Signal nicht mit vorhandenen C++-Slots verwenden die Zeichenketten erwarten.

Wie sollte denn mit Schlüsselwortargumenten die API von `pyqtSignal()` Deiner Meinung nach aussehen? Also jetzt mal von der vorhandenen API ausgehend, die ja auch damit umgehen kann, das man in C++ Methoden überladen kann.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: pyqtSignal (test_signal) der WorkClass()-Klasse mit der print_it()-Methode der Form()-Klasse verbunden, richtig? Vor diesem Hintergrund "kennt" pyqtSignal die print_it()-Methode. Und da eine Verbindung bzw. Sichtbarkeit vorhanden ist, habe ich gehofft, dass ich dann mittels Schlüsselargumente zwischen Signale und Methoden interagieren kann.

Was wäre denn an dieser Stelle dein Vorschlag?

P.S. nach längerem Suchen fand ich diese Seite. Allerdings wirkt mir die Reimplementierung nicht wirklich sicher. Aber ich habe da auch keine Ahnung. Wie siehst du das?
BlackJack

`test_signal` ist mit der `print_it()`-Methode verbunden, aber Hoffnung darauf das etwas so funktioniert wie man sich das wünscht obwohl es anders dokumentiert und implementiert ist, nützt halt nichts. Da steht eine C++-Bibliothek dahinter und das Signal/Slot-Konzept von Qt. Das geht von einer statisch typisierten Programmiersprache mit überladbaren Methoden aus. Eben C++. Dementsprechend muss man, wie man bei Signaldeklaration bei der originalen Qt-Bibliothek, auch eine oder mehrere Typsignaturen angeben und das auch ohne dynamische Schlüsselwort-Argumente. Und die Signatur vom Slot muss zu der, beziehungsweise *einer*, vom Signal übereinstimmen. Was in Python dann allerdings erst zur Laufzeit geprüft wird.

Mein Vorschlag wäre die API von `print_it()` und/oder `test_signal` zu überdenken.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Hast du dir mal die Seite angesehen? Ich hätte da deine Meinung zu.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Ich habe mir deinen Rat herangezogen, und meine API ein wenig umgeändert. Allerdings bin ich damit nicht wirklich zufrieden. Was habe ich gemacht? Ich habe in der Form()-Klasse eine weitere Methode namens intermediate_f() hinzugefügt. Diese Funktion dient (wie der englische Begriff sagt) als eine Art Zwischen-Methode für die f()-Methode. In diesem Zuge wurde test_signal() mit dieser Zwischen-Methode verbunden. Mit einem Blick in die WorkClass()-Klasse sehen wir, dass beim Senden des Signals ein Wörterbuch übertragen wird. Nun einen Blick in die Form()-Klasse. In der intermediate_f()-Methode wird das Wörterbuch als einen normalen Parameter entgegen genommen, und dieses dann an die f()-Methode weitergegeben. Allerdings erscheinen beim Funktionsaufruf der f()-Methode gleich zwei Sterne (**). Auf diese Weise wird das Wörterbuch im Zuge der Übergabe entpackt . Die Schlüssel des Wörterbuches stimmen mit den Schlüsselargumenten der f()-Methode überein. Auf diese Weise werden die Werte des Wörterbuches den jeweiligen Schlüsselargumenten zugeordnet.

Allerdings wirkt diese Lösung etwas unsauber. Oder ich bilde mir das nur ein.

Code: Alles auswählen

from sys import argv

from PyQt4.QtCore import Qt, pyqtSignal, QObject
 
from PyQt4.QtGui import QDialog, QApplication, QPushButton, \
     QFormLayout

class WorkClass(QObject):
    
    test_signal = pyqtSignal(object)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

    def run(self):
        self.test_signal.emit({"arg3": 3, "arg2": "two", "arg1":5})
            
class Form(QDialog):

    start_work_class_signal = pyqtSignal()
    
    def __init__(self, parent=None):
        QDialog.__init__(self, parent)
        
        self.init_ui()

    def init_ui(self):

       
        self.pushButton_pyqt_signal = QPushButton()
        self.pushButton_pyqt_signal.setText("pyqtSignal") 
        
        layout = QFormLayout()
        layout.addWidget(self.pushButton_pyqt_signal)
        
        self.setLayout(layout)
        self.setWindowTitle("Testing window")

        self.pushButton_pyqt_signal.clicked.connect(self.start_with_work)

    def f(self, arg1=None, arg2= None, arg3=None, arg4=None):
        print "arg1", arg1
        print "arg2", arg2
        print "arg3", arg3
        print "arg4", arg4
        
    def intermediate_f(self, arg):
        self.f(**arg)
    
    def start_with_work(self):
        work_class = WorkClass()
        work_class.test_signal.connect(self.intermediate_f)
        
        self.start_work_class_signal.connect(work_class.run)
        self.start_work_class_signal.emit()

app = QApplication(argv)
form = Form()
form.show()
app.exec_()
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Die Idee mit dem Wörterbuch geht in die richtige Richtung. Allerdings würde ich das eher über eine Ableitung auf pyqtSignal bzw. der emit-Funktion machen. Falls Du Unterstützung in C++ dafür brauchst, solltest Du das Wörterbuch durch ein QMap ersetzen und die Schnittstelle/Benutzung dafür dokumentieren. Für alle nötige Übersetzungsmagie zwischen keyword-Signal und Qt-Signal liefert Python genug Introspektionsmöglichkeiten, um das automatisch zu machen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

jerch hat geschrieben:@Sophus:
Die Idee mit dem Wörterbuch geht in die richtige Richtung. Allerdings würde ich das eher über eine Ableitung auf pyqtSignal bzw. der emit-Funktion machen.
@jerch: Wie genau meinst du das?
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: wo ist das Problem, einen Wrapper für Signale zu schreiben?

Code: Alles auswählen

class KeywordSignal(pyqtSignal):
    def __init__(self):
        pyqtSignal.__init__(self, object)
        
    def emit(self, *args, **kw):
        pyqtSignal.emit(self, (args, kw))
        
    def connect(self, func):
        def wrapper(self, args):
            func(*args[0], **args[1])
        pyqtSignal.connect(self, wrapper)
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: Das Problem ist, dass ich nicht aufi die Idee käme, die connect()-Methode so zu gestalten wie du es gemacht hast. Ehrlich gesagt, verstehe ich diese Methode in ihren einzelnen Bestandteilen nicht einmal.
Um das zu beweisen, versuche ich diese besagte Methode mal in meinen Worten zu beschreiben:

Code: Alles auswählen

    def connect(self, func):
        def wrapper(self, args):
            func(*args[0], **args[1])
        pyqtSignal.connect(self, wrapper)
In Zeile 1 wird die connect()-Methode definiert. Diese Methode nimmt immer eine Funktion entgegen, daher wird der zu übergebende Parameter func benannt. In Zeile 2 wird die wrapper()-Methode innerhalb der connect()-Methode definiert. Frage an dieser Stelle: Wieso so verschachtelt? Wäre es nicht leserlicher, wenn man die wrapper()-Methode separat, so wie die anderen beiden Methode, definiert hätte? Ich kapiere diese Verschachtelung nicht. Nun, die wrapper()-Methode nimmt nur einen normalen Parameter entgegen. JETZT wird es für mich kniffig, das ganze zu kapieren. In Zeile 3 wird in der wrapper()-Methode die entgegengenommene Funktion aufgerufen. Beim Aufruf werden Positionsargumente (*args[0]) und Schlüsselargumente (args[1]) übergeben. Frage nun, wieso wird einmal [0] und [1] verwendet? Und die letzte Zeile, also Zeile 4, kapiere ich gar nicht. pyqtSignal wird mit der wrapper()-Methode verbunden? Warum?

Du siehst, wo bei mir das Problem ist, einen solchen Wrapper zu schreiben.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Siriu3: Leider kann ich meinen vorherigen Beitrag nicht mehr bearbeiten - die Zeit ist abgelaufen. Ich habe versucht, deinen Wrapper zu verstehen, und diesen dann in mein Beispiel eingebunden.

Code: Alles auswählen

from sys import argv
 
from PyQt4.QtCore import Qt, pyqtSignal, QObject
 
from PyQt4.QtGui import QDialog, QApplication, QPushButton, \
     QFormLayout

class KeywordSignal(pyqtSignal):
    def __init__(self):
        pyqtSignal.__init__(self, object)
       
    def emit(self, *args, **kw):
        pyqtSignal.emit(self, (args, kw))
       
    def connect(self, func):
        def wrapper(self, args):
            func(*args[0], **args[1])
        pyqtSignal.connect(self, wrapper)
 
class WorkClass(QObject):
   
    test_signal = KeywordSignal(object)
   
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
 
    def run(self):
        self.test_signal.emit(arg1='Argument_One',
                              arg2= 'Argument_Two',
                              arg3='Argument_Three',
                              arg4='Argument_Four')
           
class Form(QDialog):
 
    start_work_class_signal = pyqtSignal()
   
    def __init__(self, parent=None):
        QDialog.__init__(self, parent)
       
        self.init_ui()
 
    def init_ui(self):
 
       
        self.pushButton_pyqt_signal = QPushButton()
        self.pushButton_pyqt_signal.setText("pyqtSignal")
       
        layout = QFormLayout()
        layout.addWidget(self.pushButton_pyqt_signal)
       
        self.setLayout(layout)
        self.setWindowTitle("Testing window")
 
        self.pushButton_pyqt_signal.clicked.connect(self.start_with_work)
 
    def f(self, arg1=None, arg2= None, arg3=None, arg4=None):
        print "arg1", arg1
        print "arg2", arg2
        print "arg3", arg3
        print "arg4", arg4
   
    def start_with_work(self):
        work_class = WorkClass()
        work_class.test_signal.connect(self.f)
       
        self.start_work_class_signal.connect(work_class.run)
        self.start_work_class_signal.emit()
 
app = QApplication(argv)
form = Form()
form.show()
app.exec_()
Allerdings bekomme ich gleich zu Beginn des Starts folgende Meldung (Die Zeilen-Angaben stimmen auch mit den im Forum angezeigten Zeilen überein. Es handelt sich hierbei um Zeile 8, wie hier):
Traceback (most recent call last):
File "C:\Users\Sophus\Desktop\py_scripts\wrapped_signal\wrapped_sig.py", line 8, in <module>
class KeywordSignal(pyqtSignal):
TypeError: Error when calling the metaclass bases
type 'PyQt4.QtCore.pyqtSignal' is not an acceptable base type
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Leider geht es nicht mit klassischer Vererbung auf Pythonebene. Die Klasse pyqtSignal ist eine C++-Klasse, die als nicht vererbbar an Python weitergegeben wird. Du kannst Dir aber ein Workaround mittels QObject bauen, z.B. so:

Code: Alles auswählen

from PyQt4.QtCore import pyqtSignal, QObject

class MySignal(QObject):
    sig = pyqtSignal(tuple, dict)

    def connect(self, f):
        self.sig.connect(f)

    def emit(self, *args, **kwargs):
        self.sig.emit(args, kwargs)

    def resolve(self, f):
        def wrapper(*args):
            return f(args[0], *args[1], **args[2])
        return wrapper

class Test(QObject):
    m = MySignal()

    def __init__(self):
        QObject.__init__(self)
        self.m.connect(self.out)

    def run(self):
        self.m.emit('nix da', b=42)

    @m.resolve
    def out(self, a, b):
        print a, b

t = Test()
t.run()
Der Dekorator ist nötig, um ein kompatibles Interface zu dem Signal 'sig' in `MySignal` vorzuhalten und automatisch wieder zu "entpacken". In C++ ist das Signal auch benutzbar, da es zu `SIGNAL(sig(QList, QMap))` konvertiert wird (ungetestet).
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

NB:
Eigentlich sollte der Schnippel oben threadsafe sein, da das Event-Dispatching in C++ über das sig-Signal läuft. Da ich es nicht getestet habe, ist alles ohne Gewähr. Bitte selbst testen, dass der Threadkontext korrekt gewechselt wird.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Du weil mir das Interface von Sirius eigentlich besser gefällt, hier noch eine Variante ohne Dekorator:

Code: Alles auswählen

from PyQt4.QtCore import pyqtSignal, QObject

class MySignal(QObject):
    sig = pyqtSignal(tuple, dict)

    def pyconnect(self, f):
        def wrapper(*args):
            return f(*args[0], **args[1])
        self.sig.connect(wrapper)

    def connect(self, f):
        self.sig.connect(f)

    def emit(self, args, kwargs):
        self.sig.emit(args, kwargs)

    def pyemit(self, *args, **kwargs):
        self.sig.emit(args, kwargs)

class Test(QObject):
    m = MySignal()

    def __init__(self):
        QObject.__init__(self)
        self.m.pyconnect(self.out)

    def run(self):
        self.m.pyemit('nix da', b=42)

    def out(self, a, b):
        print a, b

t = Test()
t.run()
Das Remapping der Argumente für Python passiert jetzt nur innerhalb der pyconnect- und pyemit-Methoden, die anderen sind für C++-API-Kompatibilität. Falls Dir C++ egal ist, kannst Du die py-Methoden auch umbennen und die anderen löschen.
Diese Variante könnte mehr Probleme beim threading machen, da die wrapper-Funktion von MySignal zur connect-Zeit gebunden wird und MySignal am Sender und nicht am Target hängt. Bitte selbst testen.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hier eine threadsafe Variante des letzten Ansatzes (ungetestet):

Code: Alles auswählen

from PyQt4.QtCore import pyqtSignal, QObject

class Wrapper(QObject):
    def __init__(self, f):
        QObject.__init__(self, None)
        self.f = f
    def wrap(self, *args):
        return self.f(*args[0], **args[1])

class MySignal(QObject):
    sig = pyqtSignal(tuple, dict)

    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.wrappers = []

    def pyconnect(self, f):
        wrapper = Wrapper(f)
        if (f.im_self.thread() != self.thread()):
            wrapper.moveToThread(f.im_self.thread())
        self.wrappers.append(wrapper)
        self.sig.connect(wrapper.wrap)

    def connect(self, f):
        self.sig.connect(f)

    def emit(self, args, kwargs):
        self.sig.emit(args, kwargs)

    def pyemit(self, *args, **kwargs):
        self.sig.emit(args, kwargs)

class Test(QObject):
    m = MySignal()

    def __init__(self):
        QObject.__init__(self)
        self.m.pyconnect(self.out)

    def run(self):
        self.m.pyemit('nix da', b=42)

    def out(self, a, b):
        print a, b

t = Test()
t.run()
In pyconnect wird jetzt geprüft, ob die Objekte im selben Thread sind und falls nicht, das Wrapperobjekt in den Targetthread verschoben. Dadurch kann Qt auf C++-Ebene ganz normal die Eventloop-Magie mit dem sig-Signal anwenden. Das baut im Prinzip teilweise die Logik des threadesafe-Eventdispatchers von Qt nach. Keine Ahnung, ob es einen einfacheren Weg gibt.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hier nochmal auf Threadsicherheit getestet mit signals/slots in beide Richtungen und mit ein paar Änderungen am Interface:
- MySignal muss zwingend auf Exemplarebene liegen, nicht klassenweit (sonst werden die Signale dupliziert)
- MySignal braucht das Exemplar mit dem Signal als parent (sonst kann pyconnect nicht die korrekte Threadaffinität herausfinden)

Code: Alles auswählen

from PyQt4 import QtGui
from PyQt4.QtCore import pyqtSignal, QObject, QThread

class Wrapper(QObject):
    def __init__(self, f):
        QObject.__init__(self, None)
        self.f = f
    def wrap(self, *args):
        print self.thread().currentThreadId(), 'should be equal to:',
        return self.f(*args[0], **args[1])

class MySignal(QObject):
    sig = pyqtSignal(tuple, dict)
    def __init__(self, parent):
        QObject.__init__(self, parent)
        self.wrappers = []
    def pyconnect(self, f):
        wrapper = Wrapper(f)
        if (f.im_self.thread() != self.thread()):
            wrapper.moveToThread(f.im_self.thread())
        self.wrappers.append(wrapper)
        self.sig.connect(wrapper.wrap)
    def connect(self, f):
        self.sig.connect(f)
    def emit(self, args, kwargs):
        self.sig.emit(args, kwargs)
    def pyemit(self, *args, **kwargs):
        self.sig.emit(args, kwargs)

class Work(QObject):
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.done = MySignal(self)

    def work(self, a, b, c):
        print self.thread().currentThreadId(), 'should be subthread'
        self.done.pyemit('done', 'with', z='work')

class Widget(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        # MySignal must be instance not class wide and must have parent set!
        self.do = MySignal(self)

        # some work for a thread
        self.work = Work()
        self.mythread = QThread(self)
        self.work.moveToThread(self.mythread)

        # signal for do work and work done
        # always do signal slot connections after the thread binding
        self.do.pyconnect(self.work.work)
        self.work.done.pyconnect(self.done)

        # start thread
        self.mythread.start()

        # some action to trigger everything
        self.button = QtGui.QPushButton(self)
        self.button.clicked.connect(lambda : self.do.pyemit('a', 'b', c='c'))

    def done(self, x, y, z):
        print self.thread().currentThreadId(), 'should be mainthread'

    def closeEvent(self, ev):
        # shutdown thread gracefully
        self.mythread.quit()
        self.mythread.wait()

if __name__ == '__main__':
    app = QtGui.QApplication([])
    print app.thread().currentThreadId(), 'mainthread'
    win = Widget()
    win.show()
    app.exec_()
Antworten