0

I came across this issue when making 'QMenu' actions in a loop and assigning a connection with them. Here is an example:

import sys
from PyQt5 import QtWidgets


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        
        mbar = self.menuBar()
        file_menu = mbar.addMenu("File")
        
        animals = ['Dog','Badger','Bear','Fox']
        
        
        for animal in animals:
            ac = file_menu.addAction(animal)
            ac.triggered.connect(lambda: print(animal))
            
        '''Even though each action is assigned a seperate connection with the current animal,
        only the last animal is ever printed. Why?'''
                
def main():
    app = QtWidgets.QApplication([sys.argv])
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
    
    
if __name__ == '__main__':
    main()

The last variable in the loop, 'Fox', is printed for all menu entries. The solution mentioned here is to use the QMenu triggered signal instead, allowing you to store the data in the action and access it that way. Like this:

import sys
from PyQt5 import QtWidgets


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        
        mbar = self.menuBar()
        file_menu = mbar.addMenu("File")
        
        animals = ['Dog','Badger','Bear','Fox']
        
        for animal in animals:
            ac = file_menu.addAction(animal)
            ac.setData(animal)
            
        file_menu.triggered.connect(lambda a: print(a.data()))
            
        
def main():
    app = QtWidgets.QApplication([sys.argv])
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
    
    
if __name__ == '__main__':
    main()

This works fine but is a little annoying because to work in the real world I would have to implement some sort of check to make sure I was dealing with the right sort of action, for example by making a dataclass to hold the animal and checking for that dataclass instance. But then I discovered that the following approach also works:

import sys
from PyQt5 import QtWidgets


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        
        mbar = self.menuBar()
        self.file_menu = mbar.addMenu("File")
        
        animals = ['Dog','Badger','Bear','Fox']
        
        
        for animal in animals:
            self.make_menu_entry(animal)
        
    def make_menu_entry(self, animal):
        ac = self.file_menu.addAction(animal)
        ac.triggered.connect(lambda: print(animal))
                
def main():
    app = QtWidgets.QApplication([sys.argv])
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
    
    
if __name__ == '__main__':
    main()

So now I'm thinking that this a scope problem, but I don't understand it. Can anyone explain what's happening?

Alex
  • 172
  • 9
  • `ac.triggered.connect(lambda _, animal=animal: print(animal))` – musicamante Jul 19 '22 at 11:46
  • This is really weird and unexpected behavior. PyQt5 seems to do some introspection trickery, which dissolves the closure, that the lambda should™ constitute. It, however, works by passing `partial(print, animal)` to connect() instead of the lambda. – Richard Neumann Jul 19 '22 at 11:55
  • Ok, nice. So passing the variable explicitly to the lambda is similar to defining the function separately. – Alex Jul 19 '22 at 11:58
  • This answers the question: Your lambdas do not store the value of button when it is defined. The code describing the lambda function is parsed and compiled but not executed until you actually call the lambda. Whenever any of the buttons is clicked, the current value of variable button is used. At the end of the loop, button contains "gain" and this causes the behaviour you see. – Alex Jul 19 '22 at 12:03

0 Answers0