1

I have a dictionary.
I need to create buttons with keys name, and clicked slot based on value:

dic = {'a':'111', 'b':'222', 'c':'333'}

for key in dic:
    btn = QPushButton(key, self)
    btn.clicked.connect(lambda: doit(dic[key]))
    vbox.addWidget(btn)

I have all buttons with right name. And last created button behave rightly.
But all others buttons' clicked slots are also connected to the last created button do('333').

How can I make all buttons behave differently?

Qiao
  • 16,565
  • 29
  • 90
  • 117

3 Answers3

3

The anonymous function lambda: doit(dic[key]) does not evaluate key until the function is called. By that time, the for-loop has completed, and the for-loop variable key references the last key in dic.

When the anonymous function is called (when you press a button), key is looked up in the global namespace, and the current value of key is returned.

To avoid this pitfall, you can use a default argument in the lambda expression:

for key in dic:
    btn = QPushButton(key, self)
    btn.clicked.connect(lambda key=key: doit(dic[key]))
    vbox.addWidget(btn)

Default arguments are evaluated at definition-time instead of at the time when the lambda is called. By doing this, key is looked up in the local namespace of the anonymous function, rather than in the global namespace, and since the local namespace value for key is set to the default value which is different for each pass through the for-loop, you obtain the right value for key.

This is also explained in this SO answer.

Community
  • 1
  • 1
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Thank you for answer and link, I start to get it. Bwmat's method is working, but something wrong with `lambda key=key:`, and `lambda key=key: self.label.setText(key)` returns `TypeError: QLabel.setText(str): argument 1 has unexpected type 'bool'`. Without `key=key` it works as in the question. I don't understand where `bool` is came from. – Qiao May 21 '11 at 22:23
  • @Qiao: using `lambda key=key:` works if *no* argument is passed to the lambda function. This allows `key` to take on the default value. If an argument is passed to the lambda function, then `key` would be assigned that value instead. It looks like a bool is being passed to your lambda function, and being assigned to `key`. – unutbu May 21 '11 at 23:28
1

I think the problem is that when you call lambda: doit(dic[key]), it does literally that, and looks up dic[key], and at that time key is set to whatever the last item iterated through was

try this:

dic = {'a':'111', 'b':'222', 'c':'333'}

def create_connect(x):
    return lambda: doit(x)

for key in dic:
    btn = QPushButton(key, self)
    btn.clicked.connect(create_connect(dic[key]))
    vbox.addWidget(btn)
Bwmat
  • 4,314
  • 3
  • 27
  • 42
  • But how? How is this connected with `for`? Is there a name for this trick? If there is no for, there is no need for that dumb function. I thought that `lambda:` solves all such problems. But it got worse. – Qiao May 21 '11 at 21:42
1

your iteration needs keys and values of dictionary dic. You can use dict.iteritems() method.
If lambda is getting confusing, then is better to use partial.

Try this:

from PyQt4.QtGui import QApplication, QWidget, QVBoxLayout, QPushButton
from functools import partial

class MainWidget(QWidget):
    def __init__(self):
         super(MainWidget, self).__init__()

         dic = {'a':'111', 'b':'222', 'c':'333'}
         vbox = QVBoxLayout(self)

         for key,val in dic.iteritems():
             btn = QPushButton(key, self)
             btn.clicked.connect(partial(self.doit, val))
             vbox.addWidget(btn)


    def doit(self, text):
        print "%s" % text

if __name__ == "__main__":
    app = QApplication([])
    w = MainWidget()
    w.show()
    app.exec_()
pedrotech
  • 1,359
  • 10
  • 19