0

I have a simple example PyQt5 program below which creates two QPushButtons in a loop, one for each value in a list, [ "foo", "bar" ]

It sets the clicked slot to a lambda which prints the current list value.

It should print "foo" when the user clicks the foo button, and bring "bar" when the user clicks the bar button.

Instead, however, it prints "bar" when clicking either.

#!/usr/bin/env python3
import sys
import subprocess
from PyQt5 import QtCore, QtWidgets

class App(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        layout = QtWidgets.QVBoxLayout()
        self.setLayout(layout)

        for i in [ 'foo', 'bar' ]:
            btn = QtWidgets.QPushButton(i)
            btn.clicked.connect(lambda: print(i)) # print "foo" or "bar"
            layout.addWidget(btn)

qapp = QtWidgets.QApplication(sys.argv)
app = App()
app.show()
sys.exit(qapp.exec_())

The issue I'm having (I presume) is that the value of i is reassigned on each iteration of the loop, so when the user clicks on the "foo" button, the lambda has a reference to i, which was subsequently set to "bar".

As such, every time the user clicks a button, both foo and bar print bar.

I have tried using copy.copy(i) and copy.deepcopy(i):

for i in [ 'foo', 'bar' ]:
    i = copy.deepcopy(i)
    btn = QtWidgets.QPushButton(i)
    btn.clicked.connect(lambda: print(i))
    layout.addWidget(btn)

Unfortunately that has no effect.

Question:

How can I get a copy of i for use in my slot / lambda, so that subsequent iterations of my loop don't change the value in all previous iterations?

Steve Lorimer
  • 27,059
  • 17
  • 118
  • 213
  • 1
    I have run up against this exact problem in the past. you must use a factory function to force i into a local scope. `(lambda a: lambda: print(a))(i)` – Aaron Feb 22 '19 at 20:35
  • @Aaron Thanks! Funnily enough I just found the exact same solution - as far as I understand the technical term is a `closure` – Steve Lorimer Feb 22 '19 at 20:37
  • 1
    re: the solution for the dupe, `functools.partial` is very readable and explicit to the reader what the intent is, and is therefore the best solution imao (code is read much more often than it's written ;) – Aaron Feb 22 '19 at 20:42
  • 2
    @SteveLorimer The usual solution for `lambda` to make use of default arguments: `connect(lambda *args, i=i: print(i))`. – ekhumoro Feb 22 '19 at 20:45
  • 1
    @SteveLorimer the issue, specifically, is that python (as it works in most modern languages) uses *lexical scoping*. The functions you are passing in are closed over `i`, they always refer to the `i` where they were defined. That `i` will forever be the last `i` in the loop. It's not about copying objects, you only copy objects, not variables. – juanpa.arrivillaga Feb 22 '19 at 20:48

0 Answers0