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

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

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_())

person sunwarri0r    schedule 12.10.2016    source источник
comment
Как вы вызываете функцию без индикатора выполнения? Я предполагаю, что это не во вложенном цикле?   -  person UnholySheep    schedule 12.10.2016
comment
@UnholySheep Просто закомментируйте 3 строку ниже #update progressbar   -  person sunwarri0r    schedule 12.10.2016
comment
Таким образом, возникают очевидные вопросы: как вы измеряли производительность/время выполнения? Почему вы выполняете тяжелую работу в своем потоке пользовательского интерфейса (вместо отдельного потока)?   -  person UnholySheep    schedule 12.10.2016
comment
1.) Разница в производительности настолько очевидна после комментирования обновления индикатора прогресса, что я могу измерить ее с помощью любого таймера (например, моего смартфона). 2.) Причина, по которой я выполняю это в пользовательском интерфейсе, заключается в том, что я новичок в Python :-) Я не знал, что это будет быстрее, если я помещу его в другой поток. Как-то я подумал, что будет еще медленнее из-за дополнительного потока, над которым Python должен был бы работать дополнительно. Можете ли вы дать мне подсказку, как это сделать?   -  person sunwarri0r    schedule 12.10.2016


Ответы (1)


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

  1. Печать в stdout очень дорогая, особенно когда вы делаете это 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')
person ekhumoro    schedule 12.10.2016
comment
Спасибо за информацию и комментарий print() ist OK, но мне нужно обновить, показать и обновить pbar для моего приложения. Иначе я бы не задавал вопрос :-) - person sunwarri0r; 12.10.2016
comment
Извините, я не прочитал полный ответ. Внешний цикл имеет смысл, спасибо :-) Но мне нужно понять, как это сделать с потоком. - person sunwarri0r; 12.10.2016