Skip to content

PyQt5 - QThread

Python3
  • Eines der Geheimnisse, was mich schon was länger beschäftigt ist, wie man parallele Prozesse mit Python verwaltet. Dazu findet man viele Beispiele und man rätselt und rätselt.....

    Mal gehen die Beispiele nicht, mal versteht man sie nicht. Dabei habe ich nur was gesucht, was mir möglichst einfach darstellt, wie man so was grundsätzlich aufbaut. Der Rest kommt dann mit der Zeit, wenn ich lange genug damit herumgespielt habe 😊

    Ok, warum das Ganze? Ich habe in meinem Restic UI Prozesse, die was länger dauern. Ich möchte dem Anwender das signalisieren, mittels einem waitingspinnerwidget. Ok, so weit hatte ich das auch hinbekommen, nur was mache ich mit einem Ergebnis in diesem Prozess? Das muss ja irgendwie zurück 🤔

    Ich habe dann diese Code hier gefunden und zum Laufen bekommen. YEAH

    #!/usr/bin/env python3
    
    from PyQt5.QtCore import QObject, QThread
    from PyQt5.QtCore import pyqtSlot, pyqtSignal
    from PyQt5.QtWidgets import QMainWindow
    
    import sys
    import time
    from PyQt5.QtWidgets import QApplication
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            print("Base init")
    
            self.thread = QThread()
            self.w = Worker()
            self.w.finished[str].connect(self.onFinished)
            self.w.moveToThread(self.thread)
            self.thread.started.connect(self.w.work)
            self.thread.start()
    
        @pyqtSlot(str)
        def onFinished(self, i):
            #print("Base caught finished, {}".format(i))
            print("Worker end", i)
    
    class Worker(QObject):
        finished = pyqtSignal(str)
    
        def __init__(self):
            print("Worker init")
            super().__init__()
    
        def work(self):
            print("Worker work")
            time.sleep(5)
            self.finished.emit('TEXT')
    
    if __name__ == "__main__":
    
        app = QApplication(sys.argv)
        w = MainWindow()
        w.show()
    
        sys.exit(app.exec_())
    

    Das ist jetzt nicht so verdammt schwer, ein wenig hat man ja schon gelernt. Es geht ein Fenster auf, der Hauptprozess startet den Worker als eigenen Prozess. Der macht nicht viel, als 5 Sekunden zu warten und dann wenn er beendet wird, sendet er ein Signal an den Hauptprozess def onFinished(self, i): welcher dann eine Meldung in die Konsole ausgibt. Nix besonderes, aber für mich ein Ansatz meine Aufgabe zu lösen.

    Quelle: https://stackoverflow.com/questions/29243692/pyqt5-how-to-make-qthread-return-data-to-main-thread

  • Das Thema muss sitzen, also versuche ich das etwas zu vertiefen. Gestern habe ich viel Stunden damit verbracht, mein Restic UI auf das waitingspinnerwidget umzubauen. Bis auf eine Funktion, die heute noch dran kommt, habe ich alles hinbekommen 🙂 Aber, wie immer habe ich danach Fragen. Ok, fangen wir mal an.

    @pyqtSlot(str)
    def onFinished(self, i):
        #print("Base caught finished, {}".format(i))
        print("Worker end", i)
    

    Wenn wir uns den Code anschauen, kommt die Frage auf, was macht @pyqtSlot(str) ? Ich probiere viel aus, also hatte ich das in meinem Code auskommentiert und mich gewundert, warum das immer noch funktioniert!? Ok, also Dokumentation lesen.

    Connecting a signal to a decorated Python method also has the advantage of reducing the amount of memory used and is slightly faster.

    Das klärt meine Frage aber immer noch nicht komplett. Auf stackoverflow.com, das folgende gefunden.

    For your case consider it as a good practice - denoting a method as a slot for some signal.

    Also muss ich mir jetzt merken, das es weniger Speicher verbraucht und etwas schneller ist. Gut, und es ordentlicher Programmierstil(?), wenn ich die Funktionen als abhängig von Signalen kennzeichne!? Ich denke, so passt es ganz gut.

    Sehr gute Anleitungen zum Thema findet man auch immer wieder auf realpython.com. Hier z.B. zum Thema QThread. Anhand des Beispieles habe ich meinen ersten QThread heute folgendermaßen dokumentiert.

        "First Thread with documentation"
        # Worker for restic_stats
        # Create a QThread object
        self.thread = QThread()
        # Create a worker object
        self.worker1 = Worker()
        # Move worker to the thread
        self.worker1.moveToThread(self.thread)
        # Connect signals and slots
        self.thread.started.connect(self.worker1.run)
        self.worker1.stats_finished[str].connect(self.restic_stats_finished)
        self.worker1.stats_error[str].connect(self.restic_stats_error)
        "First thread end"
    

    Dann stand ich sehr schnell vor dem Problem, was mache ich bei der nächsten Funktion, die einen QThread braucht?

        # Worker for restic_backup
        self.thread2 = QThread()
        self.worker2 = Worker2()
        self.worker2.moveToThread(self.thread2)
        self.thread2.started.connect(self.worker2.run)
        self.worker2.backup_finished[str].connect(self.restic_backup_finished)
        self.worker2.backup_error[str].connect(self.restic_backup_error)
    

    Ok, das war jetzt nicht so wirklich schwierig.

  • Und hier mal ein komplettes Beispiel.

    class Worker

    Wir legen den Worker an, das ist der Prozess der die Arbeit macht und etwas länger braucht.

    class Worker(QObject):
    
        """ Worker Class for Rest function stats"""
        stats_finished = pyqtSignal(str)
        stats_error = pyqtSignal(str)
    
        def __init__(self):
            super().__init__()
    
        def run(self):
            # Restic function
    
        try:
            # long running task
        except Exception:
            # Process don't successful, send signal
            self.stats_error.emit(result.stderr)
        else:
            # Process successful, send signal
            self.stats_finished.emit(result.stdout)
        finally:
            pass
    

    in class MainWindow

    class MainWindow(QMainWindow):
        def __init__(self):
        super().__init__()
    
        "First Thread with documentation"
        # Worker for restic_stats
        # Create a QThread object
        self.thread = QThread()
        # Create a worker object
        self.worker1 = Worker()
        # Move worker to the thread
        self.worker1.moveToThread(self.thread)
        # Connect signals and slots
        self.thread.started.connect(self.worker1.run)
        self.worker1.stats_finished[str].connect(self.restic_stats_finished)
        self.worker1.stats_error[str].connect(self.restic_stats_error)
        "First thread end"
    
        ###############################################
        # Process for restic_stats is finished
        ###############################################
    
        @pyqtSlot(str)
        def restic_stats_finished(self, i):
            # Signal from worker thread without an error
    
        Spinner.stop(self)
    
        self.thread.quit()
    
        ###############################################
        # Process for restic_stats when get an error
        ###############################################
    
        @pyqtSlot(str)
        def restic_stats_error(self, i):
            # Signal from worker thread with an error!
    
            Spinner.stop(self)
    

    Funktion restic_stats

    def restic_stats(self):
        # we start the worker thread
        self.thread.start()
    
        # we start waitingspinnerwidget
        Spinner.start(self)
    

    Ich wollte gerade schreiben, das folgendes sehr wichtig ist

    self.thread.quit()
    

    da fällt mir ein Fehler auf. Kurz ausprobiert und bingo, wenn der Prozess einen Error triggert, muss natürlich auch der Prozess beendet werden. Wenn man das nicht macht, macht das Programm nicht das was es soll. Der Grund ist, das der Prozess einfach immer weiter läuft. Er MUSS beendet werden. Ich gehe dann mal in meinem Programm alles ändern 🙂

    Ok, jetzt geht die Funktion auch zweimal hintereinander und gibt auch ordentlich den Fehler aus.

    Fazit

    Ich habe wieder sehr viel gelernt und hoffe das ich es auch richtig verstanden habe 😉 Hoffe das es dem ein oder anderen Anfänger hilft. Und falls hier ein Profi mitliest und hier Blödsinn steht bitte ich um einen Kommentar, damit ich das ändern kann. Es steht schon genug Blödsinn im Netz 🙂

  • Python3 - pyqtSignal

    Python3
    1
    0 Stimmen
    1 Beiträge
    82 Aufrufe
    Niemand hat geantwortet
  • Python3 - enumerate()

    Python3
    2
    0 Stimmen
    2 Beiträge
    113 Aufrufe
    FrankMF

    Ich habe noch was Code gefunden, den ich mal dringend aufräumen musste 🙂 Wenn ich mit dem Restic UI die Snapshots anzeige, dann sieht das so aus.

    reading repository password from stdin ID Time Host Tags Paths ---------------------------------------------------------------------------- 4e769748 2021-12-31 21:48:34 frank-MS-7C37 /home/frank/Bilder 34298934 2022-01-26 20:15:53 frank-MS-7C37 /home/frank/Bilder 0d5d88f2 2022-01-27 21:05:07 frank-MS-7C37 /home/frank/Bilder f0e7d5a7 2022-01-29 13:46:12 frank-MS-7C37 /home/frank/Bilder 79918d70 2022-01-29 13:52:56 frank-MS-7C37 /home/frank/Bilder d74272a3 2022-01-30 11:54:20 frank-MS-7C37 /home/frank/Bilder 11b0b5ad 2022-01-30 11:57:44 frank-MS-7C37 /home/frank/Bilder 4a7450d2 2022-02-20 09:35:56 frank-MS-7C37 /home/frank/Bilder c1a4a51d 2022-02-20 09:36:43 frank-MS-7C37 /home/frank/Bilder d178ded5 2022-02-20 09:38:53 frank-MS-7C37 /home/frank/Bilder 997e2259 2022-02-20 16:35:57 frank-MS-7C37 /home/frank/Bilder 3320ad5c 2022-02-20 16:49:19 frank-MS-7C37 /home/frank/Bilder ---------------------------------------------------------------------------- 12 snapshots

    Mich interessieren die IDs, weil ich bei der Eingabe der IDs überprüfen möchte, ob die eingegebene ID existiert. Also muss ich das irgendwie auslesen und speichern. Das habe ich bis jetzt so gemacht.

    def id_append(self, result): # We split the result into individual lines. # Result was passed. And create an list. result_list = result.split('\n') # We count the number of repositories and search for 'source'. count = result.count(backup_data[row].source) # Offset added, first interesting line is line 3! count = count + 3 # We loop through the list and output the ID's and store in snapshot_id x = 3 while x < count: a = result_list[x] y = a[0:8] snapshot_id.append(y) x = x + 1

    Das erschien mir heute, beim erneuten Betrachten, Spaghetticode zu sein. Machen wir das mal etwas ordentlicher.

    Ich nehme die Ausgabe (stdout) und erzeuge eine Liste, jede Zeile ein Element.

    result_list = result.split('\n')

    Sieht so aus

    ['reading repository password from stdin', 'ID Time Host Tags Paths', '----------------------------------------------------------------------------', '4e769748 2021-12-31 21:48:34 frank-MS-7C37 /home/frank/Bilder', '34298934 2022-01-26 20:15:53 frank-MS-7C37 /home/frank/Bilder', '0d5d88f2 2022-01-27 21:05:07 frank-MS-7C37 /home/frank/Bilder', 'f0e7d5a7 2022-01-29 13:46:12 frank-MS-7C37 /home/frank/Bilder', '79918d70 2022-01-29 13:52:56 frank-MS-7C37 /home/frank/Bilder', 'd74272a3 2022-01-30 11:54:20 frank-MS-7C37 /home/frank/Bilder', '11b0b5ad 2022-01-30 11:57:44 frank-MS-7C37 /home/frank/Bilder', '4a7450d2 2022-02-20 09:35:56 frank-MS-7C37 /home/frank/Bilder', 'c1a4a51d 2022-02-20 09:36:43 frank-MS-7C37 /home/frank/Bilder', 'd178ded5 2022-02-20 09:38:53 frank-MS-7C37 /home/frank/Bilder', '997e2259 2022-02-20 16:35:57 frank-MS-7C37 /home/frank/Bilder', '3320ad5c 2022-02-20 16:49:19 frank-MS-7C37 /home/frank/Bilder', '----------------------------------------------------------------------------', '12 snapshots', '']

    Diesmal benutzen wir die enumerate() Funktion

    for count, value in enumerate(result_list): print(count, value)

    Das was raus kommt, sieht so aus

    0 reading repository password from stdin 1 ID Time Host Tags Paths 2 ---------------------------------------------------------------------------- 3 4e769748 2021-12-31 21:48:34 frank-MS-7C37 /home/frank/Bilder 4 34298934 2022-01-26 20:15:53 frank-MS-7C37 /home/frank/Bilder 5 0d5d88f2 2022-01-27 21:05:07 frank-MS-7C37 /home/frank/Bilder 6 f0e7d5a7 2022-01-29 13:46:12 frank-MS-7C37 /home/frank/Bilder 7 79918d70 2022-01-29 13:52:56 frank-MS-7C37 /home/frank/Bilder 8 d74272a3 2022-01-30 11:54:20 frank-MS-7C37 /home/frank/Bilder 9 11b0b5ad 2022-01-30 11:57:44 frank-MS-7C37 /home/frank/Bilder 10 4a7450d2 2022-02-20 09:35:56 frank-MS-7C37 /home/frank/Bilder 11 c1a4a51d 2022-02-20 09:36:43 frank-MS-7C37 /home/frank/Bilder 12 d178ded5 2022-02-20 09:38:53 frank-MS-7C37 /home/frank/Bilder 13 997e2259 2022-02-20 16:35:57 frank-MS-7C37 /home/frank/Bilder 14 3320ad5c 2022-02-20 16:49:19 frank-MS-7C37 /home/frank/Bilder 15 ---------------------------------------------------------------------------- 16 12 snapshots 17

    Mich interessieren nur die Zeilen mit /home/frank/Bilder Das kann man so abfragen

    if backup_data[row].source in value:

    Und jetzt nur die ID entsprechend abspeichern

    snapshot_id.append(value[0:8])

    Sieht jetzt komplett so aus

    result_list = result.split('\n') for count, value in enumerate(result_list): if backup_data[row].source in value: snapshot_id.append(value[0:8]) print(count, value[0:8])

    Somit speichert er jetzt nur noch die IDs, der folgenden Ausgabe.

    3 4e769748 4 34298934 5 0d5d88f2 6 f0e7d5a7 7 79918d70 8 d74272a3 9 11b0b5ad 10 4a7450d2 11 c1a4a51d 12 d178ded5 13 997e2259 14 3320ad5c

    Diese IDs, kann ich dann weiterverarbeiten.

  • PyQt6 - QRegularExpressionValidator

    Python3
    1
    0 Stimmen
    1 Beiträge
    520 Aufrufe
    Niemand hat geantwortet
  • Restic UI - QtWaitingSpinner unexpected type 'float'

    Restic UI
    2
    0 Stimmen
    2 Beiträge
    237 Aufrufe
    FrankMF

    Der Autor hat meine Anpassungen um ein paar Änderungen erweitert und in sein Repo eingepflegt. Der QtWaitingSpinenr ist jetzt auch 3.10 kompatibel 🤓

    1b268980-92ca-42a4-89a0-a6e4b7ab9378-grafik.png

  • Restic UI - QThread

    Restic UI
    1
    0 Stimmen
    1 Beiträge
    110 Aufrufe
    Niemand hat geantwortet
  • Django - Webframework

    Python3
    1
    0 Stimmen
    1 Beiträge
    116 Aufrufe
    Niemand hat geantwortet
  • Python3 - class BackupList

    Python3
    2
    0 Stimmen
    2 Beiträge
    229 Aufrufe
    FrankMF

    Mir hat heute jemand auf Twitter zu meinem Problem geantwortet.

    Das habe ich natürlich direkt, bei einer Tasse Kaffee, ausprobiert. Geht einwandfrei und ein Problem ist damit Vergangenheit. Bei meiner Konstruktion musste ich immer eine Länge für die Liste p vorgeben.

    p = list(range(10))

    Das wird jetzt nicht mehr benötigt. Schön 🙂

    Jetzt muss ich mir das noch in Ruhe anschauen, nachlesen und verstehen.

    Danke für den netten Hinweis!

  • Python3 - String-Operatoren

    Python3
    1
    0 Stimmen
    1 Beiträge
    140 Aufrufe
    Niemand hat geantwortet