如何从Python日志记录模块获取非阻塞/实时行为? (输出到PyQt QTextBrowser) [英] How to get non-blocking/real-time behavior from Python logging module? (output to PyQt QTextBrowser)

查看:247
本文介绍了如何从Python日志记录模块获取非阻塞/实时行为? (输出到PyQt QTextBrowser)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

描述:我编写了一个自定义日志处理程序,用于捕获日志事件并将其写入QTextBrowser对象(如下所示的工作示例代码).

Description: I have written a custom log handler for capturing log events and writing them to a QTextBrowser object (working sample code shown below).

问题:按下按钮将调用someProcess().这会将两个字符串写入logger对象.但是,字符串仅在someProcess()返回后出现.

Issue: Pressing the button invokes someProcess(). This writes two strings to the logger object. However, the strings only appear after someProcess() returns.

问题:如何使记录的字符串立即/实时显示在QTextBrowser对象中? (即,只要调用logger输出方法,就可以使用

Question: How do I get the logged strings to appear in the QTextBrowser object immediately/in real-time? (i.e. as soon as a logger output method is invoked)

from PyQt4 import QtCore, QtGui
import sys
import time
import logging
logger = logging.getLogger(__name__)

class ConsoleWindowLogHandler(logging.Handler):
    def __init__(self, textBox):
        super(ConsoleWindowLogHandler, self).__init__()
        self.textBox = textBox

    def emit(self, logRecord):
        self.textBox.append(str(logRecord.getMessage()))

def someProcess():
    logger.error("line1")
    time.sleep(5)
    logger.error("line2")

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = QtGui.QWidget()
    textBox = QtGui.QTextBrowser()
    button = QtGui.QPushButton()
    button.clicked.connect(someProcess)
    vertLayout = QtGui.QVBoxLayout()
    vertLayout.addWidget(textBox)
    vertLayout.addWidget(button)
    window.setLayout(vertLayout)
    window.show()
    consoleHandler = ConsoleWindowLogHandler(textBox)
    logger.addHandler(consoleHandler)
    sys.exit(app.exec_())

编辑:由于@abarnert的回答,我设法使用QThread编写了这段工作代码.我将QThread子类化,以便在后台线程中运行某些功能someProcess.对于信令,我不得不求助于旧式的Signal和Slots(我不确定如何在新式中进行).我创建了一个虚拟的QObject,以便能够从日志记录处理程序中发出信号.

EDIT: thanks to the answer by @abarnert, I managed to write this piece of working code using QThread. I subclassed QThread in order to run some function someProcess in a background thread. For the signalling, I had to resort to old-style Signal and Slots (I'm not sure how to do it in the new-style). I created a dummy QObject in order to be able to emit signals from the logging handler.

from PyQt4 import QtCore, QtGui
import sys
import time
import logging
logger = logging.getLogger(__name__)

#------------------------------------------------------------------------------
class ConsoleWindowLogHandler(logging.Handler):
    def __init__(self, sigEmitter):
        super(ConsoleWindowLogHandler, self).__init__()
        self.sigEmitter = sigEmitter

    def emit(self, logRecord):
        message = str(logRecord.getMessage())
        self.sigEmitter.emit(QtCore.SIGNAL("logMsg(QString)"), message)

#------------------------------------------------------------------------------
class Window(QtGui.QWidget):
    def __init__(self):
        super(Window, self).__init__()

        # Layout
        textBox = QtGui.QTextBrowser()
        self.button = QtGui.QPushButton()
        vertLayout = QtGui.QVBoxLayout()
        vertLayout.addWidget(textBox)
        vertLayout.addWidget(self.button)
        self.setLayout(vertLayout)

        # Connect button
        self.button.clicked.connect(self.buttonPressed)

        # Thread
        self.bee = Worker(self.someProcess, ())
        self.bee.finished.connect(self.restoreUi)
        self.bee.terminated.connect(self.restoreUi)

        # Console handler
        dummyEmitter = QtCore.QObject()
        self.connect(dummyEmitter, QtCore.SIGNAL("logMsg(QString)"),
                     textBox.append)
        consoleHandler = ConsoleWindowLogHandler(dummyEmitter)
        logger.addHandler(consoleHandler)

    def buttonPressed(self):
        self.button.setEnabled(False)
        self.bee.start()

    def someProcess(self):
        logger.error("starting")
        for i in xrange(10):
            logger.error("line%d" % i)
            time.sleep(2)

    def restoreUi(self):
        self.button.setEnabled(True)

#------------------------------------------------------------------------------
class Worker(QtCore.QThread):
    def __init__(self, func, args):
        super(Worker, self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)

#------------------------------------------------------------------------------
if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

推荐答案

这里的真正问题是,您要通过睡在主线程中来阻塞整个GUI 5秒钟.您不能这样做,否则将不会显示任何更新,用户将无法与您的应用进行交互,等等.日志记录问题只是该主要问题的次要后果.

The real problem here is that you're blocking the entire GUI for 5 seconds by sleeping in the main thread. You can't do that, or no updates will show up, the user won't be able to interact with your app, etc. The logging issue is just a minor sub-consequence of that major problem.

如果您的真实程序正在从第三方模块中调用某些代码,而该过程需要5秒钟或执行某些阻止操作,那么它将遇到完全相同的问题.

And if your real program is calling some code from a third-party module that takes 5 seconds or does something blocking, it will have the exact same problem.

通常,有两种方法可以缓慢运行,即在不阻止GUI(或其他基于事件循环的)应用程序的情况下阻止事物:

In general, there are two ways to do slow, blocking things without blocking a GUI (or other event-loop-based) app:

  1. 在后台线程中进行工作.取决于您的GUI框架,通常不能从后台线程直接在GUI上调用函数或修改其对象.您必须使用某种机制将消息发布到事件循环.在Qt中,通常通过信号时隙机制来执行此操作.有关详细信息,请参见此问题.

将作业分解为快速返回的非阻塞或仅保证非常短期的阻塞作业,每个作业都在返回前安排下一个正确的作业. (对于某些GUI框架,您可以通过调用safeYield之类的东西或递归地调用事件循环来进行等效的内联,但是Qt则不行.)

Break the job up into non-blocking or guaranteed-only-very-short-term-blocking jobs that return quickly, each scheduling the next right before returning. (With some GUI frameworks, you can do the equivalent in-line by calling something like safeYield or calling the event loop recursively, but you don't do that with Qt.)

鉴于someProcess是一些您无法修改的外部代码,它可能需要花费几秒钟才能完成或执行某些阻止操作,因此您无法使用选项2.因此,选项1是:在后台运行线程.

Given that someProcess is some external code that you can't modify, which either takes seconds to finish or does something blocking, you can't use option 2. So, option 1 it is: run it in a background thread.

幸运的是,这很容易. Qt可以做到这一点,但是Python的方法甚至更简单:

Fortunately, this is easy. Qt has ways to do this, but Python's ways are even easier:

t = threading.Thread(target=someProcess)
t.start()

现在,您需要更改ConsoleWindowLogHandler.emit,以便与其直接修改textBox,它发送一个信号以在主线程中完成该操作.有关所有详细信息,请参见线程和QObject ,其中有些很好例子.

Now, you need to change ConsoleWindowLogHandler.emit so that, instead of directly modifying textBox, it sends a signal to get that done in the main thread. See Threads and QObjects for all the details, and some good examples.

更具体地讲:Mandelbrot示例使用的RenderThread实际上没有绘制任何内容,而是发送了renderedImage信号. MandelbrotWidget然后有一个updatePixmap插槽,它连接到renderedImage信号.同样,您的日志处理程序实际上不会更新文本框,而是发送gotLogMessage信号;那么您将拥有一个带有updateLog插槽的LogTextWidget,该插槽连接到该信号.当然,对于您的简单情况,只要将两侧通过信号插槽连接而不是直接方法调用连接起来,就可以将它们放在一个类中.

More concretely: The Mandelbrot example uses a RenderThread that doesn't actually draw anything, but instead sends a renderedImage signal; the MandelbrotWidget then has an updatePixmap slot that it connects to the renderedImage signal. In the same way, your log handler wouldn't actually update the text box, but instead send a gotLogMessage signal; then you'd have a LogTextWidget with a updateLog slot that it connects to that signal. Of course for your simple case, you can keep them together in a single class, just as long as you connect the two sides up with a signal-slot connection rather than a direct method call.

您可能想要将t保留在某处,并在关机时join将其保留,或设置t.daemon = True.

You probably want to either keep t around somewhere and join it during shutdown, or set t.daemon = True.

无论哪种方式,如果您想知道someProcess何时完成,则需要使用其他方式在完成后与您的主线程进行通信-同样,对于Qt,通常的答案是发送信号.这也使您可以从someProcess返回结果.而且您无需修改​​someProcess即可;只需定义一个调用someProcess并发出结果信号的包装器函数,然后从后台线程调用该包装器函数即可.

Either way, if you want to know when someProcess is done, you need to use other means of communicating back to your main thread when it's done—again, with Qt, the usual answer is to send a signal. And this also lets you get a result back from someProcess. And you don't need to modify someProcess to do this; just define a wrapper function that calls someProcess and signals its result, and call that wrapper function from the background thread.

这篇关于如何从Python日志记录模块获取非阻塞/实时行为? (输出到PyQt QTextBrowser)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆