Conexión de ranuras y señales en PyQt4 en un bucle
Estoy intentando construir una calculadora con PyQt4 y conectar las señales de "clic ()" de los botones no funciona como se esperaba. Estoy creando mis botones para los números dentro de un bucle for donde intento conectarlos después.
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)
Cuando hago clic en los botones, todos imprimen '9'. ¿Por qué es así y cómo puedo solucionarlo?
Así es simplemente cómo se definen el alcance, la búsqueda de nombres y los cierres en Python.
Python solo introduce nuevos enlaces en el espacio de nombres mediante asignaciones y listas de parámetros de funciones. i
por lo tanto, en realidad no está definido en el espacio de nombres de lambda
, sino en el espacio de nombres de __init__()
. En consecuencia, la búsqueda de nombres i
en lambda termina en el espacio de nombres de __init__()
, donde i
eventualmente se vincula a 9
. Esto se llama "cierre".
Puede solucionar esta semántica que, ciertamente, no es muy intuitiva (pero está bien definida) pasándola i
como argumento de palabra clave con un valor predeterminado. Como se dijo, los nombres en las listas de parámetros introducen nuevos enlaces en el espacio de nombres local, por lo que i
inside the lambda
luego se vuelve independiente de i
in .__init__()
:
self._numberButtons[i].clicked.connect(lambda checked, i=i: self._number(i))
ACTUALIZACIÓN: clicked
tiene un checked
argumento predeterminado que anularía el valor de i
, por lo que debe agregarse a la lista de argumentos antes del valor de la palabra clave.
Una alternativa más legible y menos mágica es functools.partial
:
self._numberButtons[i].clicked.connect(partial(self._number, i))
Estoy usando una sintaxis de señal y ranura de estilo nuevo aquí simplemente por conveniencia, la sintaxis de estilo antiguo funciona igual.
Estás creando cierres. Los cierres realmente capturan una variable, no el valor de una variable. Al final de __init__
, i
está el último elemento de range(0, 10)
, es decir 9
. Todas las lambdas que creó en este ámbito se refieren a esto i
y solo cuando se invocan, obtienen el valor de i
en el momento en que se invocan (sin embargo, las invocaciones separadas de __init__
crear lambdas se refieren a variables separadas).
Hay dos formas populares de evitar esto:
- Usando un parámetro predeterminado:
lambda i=i: self._number(i)
. Esto funciona porque los parámetros predeterminados vinculan un valor en el momento de la definición de la función. - Definición de una función auxiliar
helper = lambda i: (lambda: self._number(i))
y usohelper(i)
en el bucle. Esto funciona porque el "externo"i
se evalúa en el momentoi
en que se vincula y, como se mencionó anteriormente, el próximo cierre creado en la siguiente invocación dehelper
se referirá a una variable diferente.