Python3, PyQt5: обновление QProgressBar делает реальную задачу очень медленной

#python #python-3.x #pyqt5 #signals-slots #qprogressbar

#python #python-3.x #pyqt5 #сигналы-слоты #qprogressbar

Вопрос:

Я использую Python 3.5, PyQt5 на OSX, и мне было интересно, есть ли возможность обновить QProgressBar без замедления всей вычислительной работы. Вот мой код, и если бы я выполнил только задачу без обновления progressbar, это было намного быстрее!!

 from PyQt5.QtWidgets import (QWidget, QProgressBar, QPushButton, QApplication)
from jellyfish import levenshtein_distance, jaro_winkler
import sys

class Example(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.pbar = QProgressBar(self)
        self.pbar.setGeometry(30, 40, 200, 25)
        self.btn = QPushButton('Start', self)
        self.btn.move(40, 80)
        self.btn.clicked.connect(self.doAction)
        self.setGeometry(300, 300, 280, 170)
        self.show()


    def doAction(self):

        #setup variables
        step = 0
        m = 1000
        n = 500
        step_val = 100 / (m * n)

        #make task
        for i in range(m):
            for j in range(n):
                jaro_winkler(str(i), str(j))

                #show task
                print(i,j)

                #update progressbar
                step  = step_val
                self.pbar.setValue(step)
                QApplication.processEvents()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())
  

Затем с помощью пользователей stackoverflow я получил подсказку создать отдельный рабочий поток и подключить сигнал обновления к графическому интерфейсу. Я сделал это, и теперь это выглядит как следующий код. Это также работает и намного быстрее, но я не могу понять, как подключить излучаемый сигнал к графическому интерфейсу. Может кто-нибудь, пожалуйста, помочь мне? Заранее большое спасибо!

 from jellyfish import jaro_winkler
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QProgressBar, QMainWindow
import time
import numpy as np

class Main_Window(QMainWindow):

    def __init__(self):
        super(Main_Window,self).__init__()
        self.initUI()

    def initUI(self):
        self.pbar = QProgressBar(self)
        self.pbar.setGeometry(30, 40, 200, 25)
        self.btn = QPushButton('Start', self)
        self.btn.move(40, 80)
        self.btn.clicked.connect(MyThread.doAction)
        self.setGeometry(300, 300, 280, 170)
        self.show()

    def updateProgressBar(self, val):
        self.pbar.setValue.connect(val)


class MySignal(QWidget):
    pbar_signal = QtCore.pyqtSignal(int)


class MyThread(QtCore.QThread):
    def __init__(self):
        super().__init__()

    def doAction(self):
        t = time.time()     #for time measurement

        #setup variables
        step = 0
        m = 1000
        n = 500
        pbar_val = 100 / m
        signal_instance = MySignal()

        #make task
        for i in range(m):
            for j in range(n):
                jaro_winkler(str(i), str(j))
            signal_instance.pbar_signal.emit(pbar_val)

        #measuring task time
        print(np.round_(time.time() - t, 3), 'sec elapsed')


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    w = Main_Window()
    sys.exit(app.exec_())
  

Комментарии:

1. Как вы вызываете функцию без progressbar? Я предполагаю, что это не во вложенном цикле?

2. @UnholySheep Просто прокомментируйте 3 строки ниже #update progressbar

3. Итак, очевидные вопросы: как вы измеряли производительность / время выполнения? Почему вы выполняете тяжелую работу в своем потоке пользовательского интерфейса (вместо отдельного потока)?

4. 1.) Разница в производительности настолько очевидна, что после комментирования обновления progressbar я могу измерить ее с помощью любого устройства с таймером (например, моего смартфона). 2.) Причина, по которой я выполняю это в пользовательском интерфейсе, заключается в том, что я новичок в Python 🙂 Я не знал, что было бы быстрее, если бы я поместил ее в другой поток. Почему-то я думал, что это будет еще медленнее из-за дополнительного потока, над которым Python должен был бы работать дополнительно. Можете ли вы дать мне подсказку, как это сделать?

Ответ №1:

Есть три вещи, замедляющие работу кода:

  1. Печать в стандартный вывод очень дорогая, особенно когда вы делаете это 500 000 раз! В моей системе комментирование print(i,j) примерно вдвое сокращает время doAction , необходимое для запуска.
  2. Также довольно дорого вызывать processEvents 500 000 раз. Комментирование QApplication.processEvents() сокращает время выполнения еще на две трети.
  3. Комментирование self.pbar.setValue(step) снова сокращает время вдвое.

Надеюсь, к настоящему времени должно быть очевидно, что попытка обновить графический интерфейс 500 000 раз во время задачи, которая должна занимать менее секунды, является огромным излишеством! Время реакции большинства пользователей составляет в лучшем случае около 200 мс, поэтому вам нужно обновлять графический интерфейс примерно раз в 100 мс.

Учитывая это, одно простое решение — переместить обновления во внешний цикл:

     for i in range(m):
        for j in range(n):
            jaro_winkler(str(i), str(j))

            # show task
            # print(i,j)

            step  = step_val

        # update progressbar
        self.pbar.setValue(step)
        QApplication.processEvents()
  

Но еще лучшим решением было бы переместить вычисления в отдельный рабочий поток и заставить его периодически выдавать пользовательский сигнал для обновления индикатора выполнения:

 class Main_Window(QMainWindow):
    ...
    def initUI(self):
        ...
        self.btn.clicked.connect(self.doAction)
        self.thread = MyThread()
        self.thread.pbar_signal.connect(self.pbar.setValue)

    def doAction(self):
        if not self.thread.isRunning():
            self.thread.start()    

class MyThread(QtCore.QThread):
    pbar_signal = QtCore.pyqtSignal(int)

    def run(self):
        #for time measurement
        t = time.time()

        #setup variables
        m = 1000
        n = 500
        progress = step = 100 / m

        #make task
        for i in range(m):
            for j in range(n):
                jaro_winkler(str(i), str(j))
            progress  = step
            self.pbar_signal.emit(progress)

        #measuring task time
        print(np.round_(time.time() - t, 3), 'sec elapsed')
  

Комментарии:

1. Спасибо за информацию и print() комментарии, все в порядке, но мне нужно обновить, показать и обновить pbar для моего приложения. В противном случае я бы не задавал вопрос 🙂

2. Извините, я не прочитал полный ответ. Внешний цикл имеет смысл, спасибо 🙂 Но я должен выяснить, как это сделать с потоком.