Подключение слотов и сигналов в PyQt4 в цикле

#python #qt #pyqt #pyqt5

Вопрос:

Я пытаюсь создать калькулятор с PyQt4, и подключение сигналов ‘clicked ()’ от кнопок работает не так, как ожидалось. Я создаю свои кнопки для чисел внутри цикла for, где я пытаюсь подключить их впоследствии.

 def __init__(self):
    for i in range(0,10):
        self._numberButtons  = [QPushButton(str(i), self)]
        self.connect(self._numberButtons[i], SIGNAL('clicked()'), lambda : self._number(i))

def _number(self, x):
    print(x)
 

Когда я нажимаю на кнопки, все они выводят ‘9’.
Почему это так и как я могу это исправить?

Ответ №1:

Это просто то, как в Python определяются область видимости, поиск имен и замыкания.

Python вводит новые привязки в пространство имен только через присваивание и списки параметров функций. i поэтому фактически определяется не в пространстве имен the lambda , а в пространстве имен of __init__() . Поиск имени i в лямбда-выражении, следовательно, заканчивается в пространстве __init__() имен , к которому i в конечном итоге привязывается 9 . Это называется «замыканием».

Вы можете обойти эту, по общему признанию, не совсем интуитивную (но четко определенную) семантику, передав i в качестве аргумента ключевое слово со значением по умолчанию. Как уже было сказано, имена в списках параметров вводят новые привязки в локальное пространство имен, поэтому i внутри lambda then становится независимым от i in .__init__() :

 self._numberButtons[i].clicked.connect(lambda i=i: self._number(i))
 

Более читаемая, менее волшебная альтернатива functools.partial :

 self._numberButtons[i].clicked.connect(partial(self._number, i))
 

Я использую синтаксис сигналов и слотов нового стиля здесь просто для удобства, синтаксис старого стиля работает точно так же.

Комментарии:

1. Спасибо. Я пойду с functools.частичное решение.

Ответ №2:

Вы создаете замыкания. Замыкания действительно фиксируют переменную, а не значение переменной. В конце __init__ , i является последним элементом range(0, 10) , т.е. 9 . Все лямбды, которые вы создали в этой области, ссылаются на this i , и только когда они вызываются, они получают значение i в момент их вызова (однако отдельные вызовы __init__ create lambdas ссылаются на отдельные переменные!).

Есть два популярных способа избежать этого:

  1. Использование параметра по умолчанию: lambda i=i: self._number(i) . Это работает, потому что параметры по умолчанию привязывают значение во время определения функции.
  2. Определение вспомогательной функции helper = lambda i: (lambda: self._number(i)) и ее использование helper(i) в цикле. Это работает, потому что «внешний» i вычисляется во время i привязки, и — как упоминалось ранее — следующее замыкание, созданное при следующем helper вызове, будет ссылаться на другую переменную.

Ответ №3:

Используйте Qt способ, используйте QSignalMapper вместо этого.

Комментарии:

1. Пожалуйста, не делайте этого. QSignalMapper является остатком C 98, в котором нет лямбд или частичных функций и где такой обходной путь является необходимым злом. Однако в Python QSignalMapper это просто лишнее и действительно раздутое по сравнению с functools.partial() or lambda. Решения с partial() or lambda короче и элегантнее, чем QSignalMapper .

2. Ну, что касается выбора, я бы выбрал QSignalMapper совместимость с Qt / C .

3. Конечно, речь идет о выборе, но я просто не понимаю вашего выбора и не могу следовать ему. Я использую Python для приложений Qt именно потому, что это не C , и если бы я отказался от всех специальных функций Python для поддержания или достижения совместимости с C , я мог бы с таким же успехом использовать C 😉