Python: How do I prevent delayed expansion in the QTimer.singleShot(..) function?

Question:

I often use the QTimer.singleShot(..) function to execute a certain action/function with some delay. Sure, I could do it with a time.sleep(..) as well, but that would freeze my user interface.

Sometimes you want to hand over a parameter to the delayed function. That’s where the lambda feature of Python comes in handy. Take a look at the following example:

    #######################################
    #   Print 'Hello world' vertically    #
    #######################################
    myString = 'Hello world'
    i = 0
    for c in myString:
        QtCore.QTimer.singleShot(10 + i*100, lambda: print(str(c)))
        i += 1

One would intuitively expect the output shown on the left. But you actually get a vertical list of character ‘d’ – which is (surprise) the last character in the given string.

         _                                _
    H     |                          d     |
    e     |                          d     |
    l     |                          d     |
    l     |                          d     |
    o     |   Result you             d     |   Result you
           >  would expect           d      >  actually get!
    w     |                          d     |
    o     |                          d     |
    r     |                          d     |
    l     |                          d     |
    d     |                          d     |
         _|                               _|

It seems like the QTimer.singleshot(..) function applies the ‘delayed expansion’ principle. That’s great in some situations, but not always.

How do you get control over the expansion? Can you force immediate expansion?


Example code :

Copy-paste the following code into a fresh python file, and run it from the shell.

    import sys
    from PyQt4 import QtGui, QtCore

    class MainForm(QtGui.QMainWindow):
        def __init__(self, parent=None):
            super(MainForm, self).__init__(parent)

            # create button
            self.button = QtGui.QPushButton("QTimer.singleShot(..)", self)
            self.button.clicked.connect(self.btnAction)
            self.button.resize(200, 30)


        def btnAction(self):
            myString = 'Hello world'
            i = 0
            for c in myString:
                QtCore.QTimer.singleShot(10 + i*100, lambda: print(str(c)))
                i += 1


    def main():
        app = QtGui.QApplication(sys.argv)
        form = MainForm()
        form.show()
        app.exec_()

    if __name__ == '__main__':
        main()

You will see the ‘Hello world’ example in your shell when you click the button:

enter image description here

Asked By: K.Mulier

||

Answers:

I think I found the answer. The functools.partial(..) library allows you to do exactly the same as with the lambda:.. feature, except that the arguments get freezed 🙂

import sys
from PyQt4 import QtGui, QtCore
import functools



class MainForm(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(MainForm, self).__init__(parent)

        # create button
        self.button = QtGui.QPushButton("QTimer.singleShot(..)", self)
        self.button.clicked.connect(self.btnAction)
        self.button.resize(200, 30)


    def btnAction(self):
        myString = 'Hello world'
        i = 0
        for c in myString:
            #QtCore.QTimer.singleShot(10 + i*100, lambda: print(str(c)))
            QtCore.QTimer.singleShot(10 + i * 100, functools.partial(print, str(c)))
            i += 1


def main():
    app = QtGui.QApplication(sys.argv)
    form = MainForm()
    form.show()
    app.exec_()

if __name__ == '__main__':
    main()

EDIT :

Thank you @ekhumoro for your comment. Your code line works as well:

QtCore.QTimer.singleShot(10 + i*100, lambda arg=c: print(str(arg)))
Answered By: K.Mulier
Categories: questions Tags: , , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.