Is it safe to assume that using a QEventLoop is a correct way of creating qt5-compatible coroutines in python?

huangapple go评论69阅读模式
英文:

Is it safe to assume that using a QEventLoop is a correct way of creating qt5-compatible coroutines in python?

问题

我在我的代码中使用自定义的QEventLoop实例来模拟QDialog.exec_()函数的行为。也就是说,我可以在某个点上暂停Python脚本的执行,而不会冻结GUI,然后,在用户手动与GUI交互后的某个时刻,通过调用QEventLoop.quit(),程序会在QEventLoop.exec_()调用后恢复执行。这正是协程应该具备的行为。

为了说明这个例子,这是我正在做的一个最小可重现示例(MRE):

from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QRadioButton, QButtonGroup, QDialogButtonBox
from PySide2.QtCore import Qt, QTimer, QEventLoop

recursion = 5

def laterOn():
    # 从用户那里获取设置:
    dialog = SettingsForm()

    # 模拟一个协程。
    # - Python解释器在此行上暂停。
    # - 其他小部件的代码仍将执行,因为它们是从Qt5侧连接的。
    dialog.exec_()

    # 在事件循环退出后,Python解释器将从被暂停的地方继续执行:

    # 使用对话框的结果:
    if dialog.result:
        # 我们可以以某种方式使用用户的响应。在这个简单的例子中,我只是在控制台上打印文本。
        print('SELECTED OPTION WAS: ', dialog.group.checkedButton().text())

class SettingsForm(QWidget):
    def __init__(self):
        super().__init__()
        vbox = QVBoxLayout()
        self.setLayout(vbox)
        self.eventLoop = QEventLoop()
        self.result = False

        a = QRadioButton('A option')
        b = QRadioButton('B option')
        c = QRadioButton('C option')

        self.group = QButtonGroup()
        self.group.addButton(a)
        self.group.addButton(b)
        self.group.addButton(c)

        bbox = QDialogButtonBox()
        bbox.addButton('Save', QDialogButtonBox.AcceptRole)
        bbox.addButton('Cancel', QDialogButtonBox.RejectRole)
        bbox.accepted.connect(self.accept)
        bbox.rejected.connect(self.reject)

        vbox.addWidget(a)
        vbox.addWidget(b)
        vbox.addWidget(c)
        vbox.addWidget(bbox)

        global recursion
        recursion -= 1
        if recursion > 0:
            QTimer.singleShot(0, laterOn)

    def accept(self):
        self.close()
        self.eventLoop.quit()
        self.result = True

    def reject(self):
        self.close()
        self.eventLoop.quit()
        self.result = False

    def exec_(self):
        self.setWindowModality(Qt.ApplicationModal)
        self.show()
        self.eventLoop.exec_()

app = QApplication()

# 初始化小部件、主界面等...
mwin = QWidget()
mwin.show()

QTimer.singleShot(0, laterOn)

app.exec_()

在这段代码中,recursion 变量控制创建多少个不同的QEventLoop实例,以及调用多少次其.exec_()方法,从而暂停Python解释器的执行,而不会冻结其他小部件。


可以看到,QEventLoop.exec_()的行为就像Python生成器函数中的yield关键字一样。是否可以正确地假设每次调用QEventLoop.exec()时都使用了yield?或者这与协程无关,背后发生的是另一件事?(我不知道是否有办法查看PySide2的源代码,所以我在问这个问题。)

英文:

I'm using a custom QEventLoop instance in my code to simulate the QDialog.exec_() function. That is, the ability to pause the python script at some point without freezing the GUI, and then, at some other point of time after the user manually interacts with the GUI, the program resumes its execution right after the QEventLoop.exec_() call, by calling QEventLoop.quit(). This is the exact behaviour of what a coroutine should look like.

To illustrate the example, here's a MRE of what I'm doing:

from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QRadioButton, QButtonGroup, QDialogButtonBox
from PySide2.QtCore import Qt, QTimer, QEventLoop
recursion = 5
def laterOn():
# Request settings from user:
dialog = SettingsForm()
# Simulate a coroutine.
# - Python interpreter is paused on this line.
# - Other widgets code will still execute as they are connected from Qt5 side.
dialog.exec_()
# After the eventloop quits, the python interpreter will execute from where
# it was paused:
# Using the dialog results:
if (dialog.result):
# We can use the user's response somehow. In this simple example, I'm just
# printing text on the console.
print('SELECTED OPTION WAS: ', dialog.group.checkedButton().text())
class SettingsForm(QWidget):
def __init__(self):
super().__init__()
vbox = QVBoxLayout()
self.setLayout(vbox)
self.eventLoop = QEventLoop()
self.result = False
a = QRadioButton('A option')
b = QRadioButton('B option')
c = QRadioButton('C option')
self.group = QButtonGroup()
self.group.addButton(a)
self.group.addButton(b)
self.group.addButton(c)
bbox = QDialogButtonBox()
bbox.addButton('Save', QDialogButtonBox.AcceptRole)
bbox.addButton('Cancel', QDialogButtonBox.RejectRole)
bbox.accepted.connect(self.accept)
bbox.rejected.connect(self.reject)
vbox.addWidget(a)
vbox.addWidget(b)
vbox.addWidget(c)
vbox.addWidget(bbox)
global recursion
recursion -= 1
if (recursion > 0):
QTimer.singleShot(0, laterOn)
def accept(self):
self.close()
self.eventLoop.quit()
self.result = True
def reject(self):
self.close()
self.eventLoop.quit()
self.result = False
def exec_(self):
self.setWindowModality(Qt.ApplicationModal)
self.show()
self.eventLoop.exec_()
###
app = QApplication()
# Initialize widgets, main interface, etc...
mwin = QWidget()
mwin.show()
QTimer.singleShot(0, laterOn)
app.exec_()

In this code, the recursion variable control how many times different instances of QEventLoop are crated, and how many times its .exec_() method is called, halting the python interpreter without freezing the other widgets.


It can be seen that the QEventLoop.exec_() acts just like a yield keyword from a python generator function. Is it correct to assume that yield is used every time QEventLoop.exec() is called? Or it's not something related to a coroutine at all, and another thing is happening at the background? (I don't know if there's a way to see the PySide2 source code, so that's why I'm asking.)

答案1

得分: 1

我相信你不完全理解事件驱动编程的工作原理。

从根本上说,有一个无限循环,只等待任何事件发生。

通常有一个事件队列,通常为空,直到发生某些事情,然后它会处理每个事件,直到队列再次为空。每个事件最终会触发某些操作,通常是通过函数调用来实现的。

这个循环将会“阻塞”在同一函数块内循环后面存在的任何东西的执行,但这并不会阻止它调用自己的函数。

考虑这个简单的例子:

queue = []

def myLoop(index):
    running = True
    while running:
        queue.extend(getSystemEvents())
        while queue:
            event = queue.pop(0)
            if event == 1:
                doSomething(index)
            elif event == 10:
                myLoop(index + 1)
            elif event < 0:
                running = False
                break

    print('exiting loop', index)

def doSomething(index):
    print('Hello there!', index)

def getSystemEvents():
    # 一些获取系统事件并返回它们的库

myLoop()
print('program ended')

现在发生的情况是,Qt应用程序与底层操作系统进行交互,并从中接收事件,这基本上就是上面的getSystemEvents()伪函数所做的事情。不仅可以在循环内处理事件并从中调用函数,甚至还可以生成一个进一步的事件循环,该事件循环也能够执行相同的操作。

严格来说,第一个myLoop()调用将会被“阻塞”,直到它退出,直到它完成,你将永远不会得到最后的print,但这些函数仍然可以被调用,因为循环本身会调用它们。

执行范围没有变化:这些函数是从事件循环中调用的,因此它们的范围实际上是嵌套在循环内的。

也没有涉及到yield(至少在严格的Python意义上没有),因为事件循环不是生成器:尽管从概念上讲它们相似,因为它们都是控制循环行为的例程,但生成器实际上是非常不同的,因为它们被认为是"半协程"; 虽然从概念上讲它们相似,但生成器只有在被主动调用时才会变得活跃(例如,当调用next()时),然后在产出后立即被阻塞,直到进一步请求另一个项。

QEventLoop(实际上由QCoreApplication的exec()本身使用)基本上就像上面的示例(包括嵌套循环),但涉及到线程、对象树和事件分发/处理方面的一些复杂性,因为可以有多层事件循环和处理程序。

这也是为什么有时文档会不建议对支持事件循环的某些窗口小部件(特别是QDialog)使用exec的原因,因为在树中的任何对象被销毁时,对象之间的关系可能会变得非常“争议”,并且可能会在嵌套循环之间的任何对象被销毁时导致意外行为。

英文:

I believe you don't completely understand how event driven programming works.

Fundamentally speaking, there is an infinite while loop that just waits for anything to happen.

There is normally an event queue which is normally empty until something happens, and then it processes each event until the queue is empty again. Each event will eventually trigger something, normally by doing a function call.

That loop will "block" the execution of anything that exists after the loop within the same function block, but that doesn't prevent it to call functions on itself.

Consider this simple example:

queue = []

def myLoop(index):
    running = True
    while running:
        queue.extend(getSystemEvents())
        while queue:
            event = queue.pop(0)
            if event == 1:
                doSomething(index)
            elif event == 10:
                myLoop(index + 1)
            elif event &lt; 0:
                running = False
                break

    print(&#39;exiting loop&#39;, index)

def doSomething(index):
    print(&#39;Hello there!&#39;, index)

def getSystemEvents():
    # some library that get system events and returns them

myLoop()
print(&#39;program ended&#39;)

Now, what happens is that the Qt application interacts with the underlying OS and receives events from it, which is fundamentally what the getSystemEvents() pseudo function above does. Not only you can process events within the loop and call functions from it, but you can even spawn a further event loop that would be able to do the same.

Strictly speaking, the first myLoop() call will be "blocked" until it exits, and you'll never get the last print until it's finished, but those functions can still be called, as the loop itself will call them.

There is no change in the execution scope: the functions are called from the event loop, so their scope is actually nested within the loop.

There is also no yield involved (at least in strict python sense), since an event loop is not a generator: while conceptually speaking they are similar in the fact that they are both routines that control the behavior of a loop, generators are actually quite different, as they are considered "semicoroutines"; while an event loop is always active (unless blocked by something else or interrupted) and it does not yield (in general programming sense), a generator becomes active only when actively called (for instance, when next() is called) and its execution is then blocked right after yielding, and will not progress until a further item is requested.

A QEventLoop (which is actually used by the QCoreApplication exec() itself) fundamentally works like the example above (nested loops included), but with some intricacies related to threading, object trees and event dispatching/handling, because there can be multiple levels of event loops and handlers.

This is also a reason for which sometimes the documentation discourages the usage of exec for some widgets that support event loops (specifically, QDialog), as there are certain situations for which the relations between objects can become extremely "controversial" and can cause unexpected behavior when any object in the tree "between" nested loops gets destroyed.

huangapple
  • 本文由 发表于 2023年6月26日 22:14:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/76557519.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定