Seite 1 von 2

Das unbarmherzige pyqtSignal()

Verfasst: Mittwoch 8. Februar 2017, 23:38
von Sophus
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.

  1. from sys import argv
  2.  
  3. from PyQt4.QtCore import Qt, pyqtSignal, QObject
  4.  
  5. from PyQt4.QtGui import QDialog, QApplication, QPushButton, \
  6.      QFormLayout
  7.  
  8. class WorkClass(QObject):
  9.    
  10.     test_signal = pyqtSignal(object)
  11.    
  12.     def __init__(self, parent=None):
  13.         QObject.__init__(self, parent)
  14.  
  15.     def run(self):
  16.         self.test_signal.emit(self.test_signal.emit('normal argument', keyword_arg_second="Ok, second"))
  17.            
  18. class Form(QDialog):
  19.  
  20.     start_work_class_signal = pyqtSignal()
  21.    
  22.     def __init__(self, parent=None):
  23.         QDialog.__init__(self, parent)
  24.        
  25.         self.init_ui()
  26.  
  27.     def init_ui(self):
  28.  
  29.        
  30.         self.pushButton_pyqt_signal = QPushButton()
  31.         self.pushButton_pyqt_signal.setText("pyqtSignal")
  32.        
  33.         layout = QFormLayout()
  34.         layout.addWidget(self.pushButton_pyqt_signal)
  35.        
  36.         self.setLayout(layout)
  37.         self.setWindowTitle("Testing window")
  38.  
  39.         self.pushButton_pyqt_signal.clicked.connect(self.start_with_work)
  40.  
  41.     def print_it(self, argument, keyword_arg_first=None, keyword_arg_second=None):
  42.         print "Do some with argument", argument
  43.         print "Do some with first keyword:", keyword_arg_first
  44.         print "Do some with second keyword", keyword_arg_second
  45.         return
  46.    
  47.     def start_with_work(self):
  48.         work_class = WorkClass()
  49.         work_class.test_signal.connect(self.print_it)
  50.        
  51.         self.start_work_class_signal.connect(work_class.run)
  52.         self.start_work_class_signal.emit()
  53.  
  54. app = QApplication(argv)
  55. form = Form()
  56. form.show()
  57. app.exec_()

Re: Das unbarmherzige pyqtSignal()

Verfasst: Mittwoch 8. Februar 2017, 23:57
von 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.

Re: Das unbarmherzige pyqtSignal()

Verfasst: Donnerstag 9. Februar 2017, 00:24
von Sophus
@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?

Re: Das unbarmherzige pyqtSignal()

Verfasst: Donnerstag 9. Februar 2017, 01:02
von 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.

Re: Das unbarmherzige pyqtSignal()

Verfasst: Donnerstag 9. Februar 2017, 02:07
von Sophus
@BlackJack: Hast du dir mal die Seite angesehen? Ich hätte da deine Meinung zu.

Re: Das unbarmherzige pyqtSignal()

Verfasst: Donnerstag 9. Februar 2017, 20:24
von Sophus
@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.

  1. from sys import argv
  2.  
  3. from PyQt4.QtCore import Qt, pyqtSignal, QObject
  4.  
  5. from PyQt4.QtGui import QDialog, QApplication, QPushButton, \
  6.      QFormLayout
  7.  
  8. class WorkClass(QObject):
  9.    
  10.     test_signal = pyqtSignal(object)
  11.    
  12.     def __init__(self, parent=None):
  13.         QObject.__init__(self, parent)
  14.  
  15.     def run(self):
  16.         self.test_signal.emit({"arg3": 3, "arg2": "two", "arg1":5})
  17.            
  18. class Form(QDialog):
  19.  
  20.     start_work_class_signal = pyqtSignal()
  21.    
  22.     def __init__(self, parent=None):
  23.         QDialog.__init__(self, parent)
  24.        
  25.         self.init_ui()
  26.  
  27.     def init_ui(self):
  28.  
  29.        
  30.         self.pushButton_pyqt_signal = QPushButton()
  31.         self.pushButton_pyqt_signal.setText("pyqtSignal")
  32.        
  33.         layout = QFormLayout()
  34.         layout.addWidget(self.pushButton_pyqt_signal)
  35.        
  36.         self.setLayout(layout)
  37.         self.setWindowTitle("Testing window")
  38.  
  39.         self.pushButton_pyqt_signal.clicked.connect(self.start_with_work)
  40.  
  41.     def f(self, arg1=None, arg2= None, arg3=None, arg4=None):
  42.         print "arg1", arg1
  43.         print "arg2", arg2
  44.         print "arg3", arg3
  45.         print "arg4", arg4
  46.        
  47.     def intermediate_f(self, arg):
  48.         self.f(**arg)
  49.    
  50.     def start_with_work(self):
  51.         work_class = WorkClass()
  52.         work_class.test_signal.connect(self.intermediate_f)
  53.        
  54.         self.start_work_class_signal.connect(work_class.run)
  55.         self.start_work_class_signal.emit()
  56.  
  57. app = QApplication(argv)
  58. form = Form()
  59. form.show()
  60. app.exec_()

Re: Das unbarmherzige pyqtSignal()

Verfasst: Mittwoch 15. Februar 2017, 13:52
von jerch
@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.

Re: Das unbarmherzige pyqtSignal()

Verfasst: Mittwoch 15. Februar 2017, 14:59
von Sophus
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?

Re: Das unbarmherzige pyqtSignal()

Verfasst: Mittwoch 15. Februar 2017, 17:34
von Sirius3
@Sophus: wo ist das Problem, einen Wrapper für Signale zu schreiben?
  1. class KeywordSignal(pyqtSignal):
  2.     def __init__(self):
  3.         pyqtSignal.__init__(self, object)
  4.        
  5.     def emit(self, *args, **kw):
  6.         pyqtSignal.emit(self, (args, kw))
  7.        
  8.     def connect(self, func):
  9.         def wrapper(self, args):
  10.             func(*args[0], **args[1])
  11.         pyqtSignal.connect(self, wrapper)

Re: Das unbarmherzige pyqtSignal()

Verfasst: Mittwoch 15. Februar 2017, 21:00
von Sophus
@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:

  1.     def connect(self, func):
  2.         def wrapper(self, args):
  3.             func(*args[0], **args[1])
  4.         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.

Re: Das unbarmherzige pyqtSignal()

Verfasst: Mittwoch 15. Februar 2017, 21:42
von Sophus
@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.

  1. from sys import argv
  2.  
  3. from PyQt4.QtCore import Qt, pyqtSignal, QObject
  4.  
  5. from PyQt4.QtGui import QDialog, QApplication, QPushButton, \
  6.      QFormLayout
  7.  
  8. class KeywordSignal(pyqtSignal):
  9.     def __init__(self):
  10.         pyqtSignal.__init__(self, object)
  11.        
  12.     def emit(self, *args, **kw):
  13.         pyqtSignal.emit(self, (args, kw))
  14.        
  15.     def connect(self, func):
  16.         def wrapper(self, args):
  17.             func(*args[0], **args[1])
  18.         pyqtSignal.connect(self, wrapper)
  19.  
  20. class WorkClass(QObject):
  21.    
  22.     test_signal = KeywordSignal(object)
  23.    
  24.     def __init__(self, parent=None):
  25.         QObject.__init__(self, parent)
  26.  
  27.     def run(self):
  28.         self.test_signal.emit(arg1='Argument_One',
  29.                               arg2= 'Argument_Two',
  30.                               arg3='Argument_Three',
  31.                               arg4='Argument_Four')
  32.            
  33. class Form(QDialog):
  34.  
  35.     start_work_class_signal = pyqtSignal()
  36.    
  37.     def __init__(self, parent=None):
  38.         QDialog.__init__(self, parent)
  39.        
  40.         self.init_ui()
  41.  
  42.     def init_ui(self):
  43.  
  44.        
  45.         self.pushButton_pyqt_signal = QPushButton()
  46.         self.pushButton_pyqt_signal.setText("pyqtSignal")
  47.        
  48.         layout = QFormLayout()
  49.         layout.addWidget(self.pushButton_pyqt_signal)
  50.        
  51.         self.setLayout(layout)
  52.         self.setWindowTitle("Testing window")
  53.  
  54.         self.pushButton_pyqt_signal.clicked.connect(self.start_with_work)
  55.  
  56.     def f(self, arg1=None, arg2= None, arg3=None, arg4=None):
  57.         print "arg1", arg1
  58.         print "arg2", arg2
  59.         print "arg3", arg3
  60.         print "arg4", arg4
  61.    
  62.     def start_with_work(self):
  63.         work_class = WorkClass()
  64.         work_class.test_signal.connect(self.f)
  65.        
  66.         self.start_work_class_signal.connect(work_class.run)
  67.         self.start_work_class_signal.emit()
  68.  
  69. app = QApplication(argv)
  70. form = Form()
  71. form.show()
  72. 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

Re: Das unbarmherzige pyqtSignal()

Verfasst: Samstag 18. Februar 2017, 19:07
von jerch
@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:
  1. from PyQt4.QtCore import pyqtSignal, QObject
  2.  
  3. class MySignal(QObject):
  4.     sig = pyqtSignal(tuple, dict)
  5.  
  6.     def connect(self, f):
  7.         self.sig.connect(f)
  8.  
  9.     def emit(self, *args, **kwargs):
  10.         self.sig.emit(args, kwargs)
  11.  
  12.     def resolve(self, f):
  13.         def wrapper(*args):
  14.             return f(args[0], *args[1], **args[2])
  15.         return wrapper
  16.  
  17. class Test(QObject):
  18.     m = MySignal()
  19.  
  20.     def __init__(self):
  21.         QObject.__init__(self)
  22.         self.m.connect(self.out)
  23.  
  24.     def run(self):
  25.         self.m.emit('nix da', b=42)
  26.  
  27.     @m.resolve
  28.     def out(self, a, b):
  29.         print a, b
  30.  
  31. t = Test()
  32. 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).

Re: Das unbarmherzige pyqtSignal()

Verfasst: Samstag 18. Februar 2017, 19:43
von jerch
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.

Re: Das unbarmherzige pyqtSignal()

Verfasst: Samstag 18. Februar 2017, 19:59
von jerch
Du weil mir das Interface von Sirius eigentlich besser gefällt, hier noch eine Variante ohne Dekorator:
  1. from PyQt4.QtCore import pyqtSignal, QObject
  2.  
  3. class MySignal(QObject):
  4.     sig = pyqtSignal(tuple, dict)
  5.  
  6.     def pyconnect(self, f):
  7.         def wrapper(*args):
  8.             return f(*args[0], **args[1])
  9.         self.sig.connect(wrapper)
  10.  
  11.     def connect(self, f):
  12.         self.sig.connect(f)
  13.  
  14.     def emit(self, args, kwargs):
  15.         self.sig.emit(args, kwargs)
  16.  
  17.     def pyemit(self, *args, **kwargs):
  18.         self.sig.emit(args, kwargs)
  19.  
  20. class Test(QObject):
  21.     m = MySignal()
  22.  
  23.     def __init__(self):
  24.         QObject.__init__(self)
  25.         self.m.pyconnect(self.out)
  26.  
  27.     def run(self):
  28.         self.m.pyemit('nix da', b=42)
  29.  
  30.     def out(self, a, b):
  31.         print a, b
  32.  
  33. t = Test()
  34. 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.

Re: Das unbarmherzige pyqtSignal()

Verfasst: Samstag 18. Februar 2017, 21:07
von jerch
Hier eine threadsafe Variante des letzten Ansatzes (ungetestet):
  1. from PyQt4.QtCore import pyqtSignal, QObject
  2.  
  3. class Wrapper(QObject):
  4.     def __init__(self, f):
  5.         QObject.__init__(self, None)
  6.         self.f = f
  7.     def wrap(self, *args):
  8.         return self.f(*args[0], **args[1])
  9.  
  10. class MySignal(QObject):
  11.     sig = pyqtSignal(tuple, dict)
  12.  
  13.     def __init__(self, parent=None):
  14.         QObject.__init__(self, parent)
  15.         self.wrappers = []
  16.  
  17.     def pyconnect(self, f):
  18.         wrapper = Wrapper(f)
  19.         if (f.im_self.thread() != self.thread()):
  20.             wrapper.moveToThread(f.im_self.thread())
  21.         self.wrappers.append(wrapper)
  22.         self.sig.connect(wrapper.wrap)
  23.  
  24.     def connect(self, f):
  25.         self.sig.connect(f)
  26.  
  27.     def emit(self, args, kwargs):
  28.         self.sig.emit(args, kwargs)
  29.  
  30.     def pyemit(self, *args, **kwargs):
  31.         self.sig.emit(args, kwargs)
  32.  
  33. class Test(QObject):
  34.     m = MySignal()
  35.  
  36.     def __init__(self):
  37.         QObject.__init__(self)
  38.         self.m.pyconnect(self.out)
  39.  
  40.     def run(self):
  41.         self.m.pyemit('nix da', b=42)
  42.  
  43.     def out(self, a, b):
  44.         print a, b
  45.  
  46. t = Test()
  47. 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.