pythongeneratorpyside2qt-slot

Why can't you use a generator function as a slot?


I'm trying to iterate through a list in a PySide2 application. So each time a "Next" button is pressed, the next item is returned from the list and displayed. I could keep track of the index of the entry in my list which was most recently read and manually increment the index on each slot call, but I thought it might be more elegant to turn the slot into a generator function. But it doesn't work.

Minimal (not) working example follows.

import sys
from PySide2.QtWidgets import QApplication, QPushButton
from PySide2.QtCore import SIGNAL, QObject

def func():
    stringEntries=["One", "Two", "Three"]
    for item in stringEntries:
        # In the application this sets the values of a numpy array 
        # and fires a signal which updates a matplotlib canvas but meh, whatever
        print("func ", item, " has been called!")
        # This sort of works without the following yield statement
        yield

app = QApplication(sys.argv)
button = QPushButton("Next")
QObject.connect(button, SIGNAL ('clicked()'), func)
button.show()                                                                                             

sys.exit(app.exec_())

I was sort of expecting a different string to be printed each time I pressed the "Next" button, but instead it just sits there mocking me...

Is anyone able to point out the thing that I've fundamentally misunderstood please?


Solution

  • As @jasonharper points out in a comment you are creating a new iterator every time the button is pressed that generates the problem, a possible solution is to create a class that has the iterator as an attribute and using the __call__ method that looks over it, to make it simple and elegant I have created a decorator:

    from PySide2.QtCore import QObject
    from PySide2.QtWidgets import QApplication, QPushButton
    
    
    class decorator:
        def __init__(self, f):
            self._f = f
            self._iterator = None
    
        def __call__(self, *args, **kwargs):
            if self._iterator is None:
                self._iterator = self._f(*args, **kwargs)
            try:
                return next(self._iterator)
            except StopIteration:
                pass
    
    
    @decorator
    def func():
        stringEntries = ["One", "Two", "Three"]
        for item in stringEntries:
            print("func ", item, " has been called!")
            yield
    
    
    if __name__ == "__main__":
        import sys
    
        app = QApplication(sys.argv)
        button = QPushButton("Next")
        button.clicked.connect(func)
        button.show()
    
        sys.exit(app.exec_())