El viernes pasado fui a una entrevista de trabajo y tuve que responder la siguiente pregunta: ¿por qué este código UnboundLocalError: local variable 'var' referenced before assignment
una excepción ( UnboundLocalError: local variable 'var' referenced before assignment
en la línea que contiene var += 1
)?
def outer(): var = 1 def inner(): var += 1 return var return inner
No pude dar una respuesta adecuada; este hecho realmente me molestó, y cuando llegué a casa me esforcé mucho por encontrar una respuesta adecuada. Bueno, he encontrado la respuesta, pero ahora hay algo más que me confunde.
Tengo que decir por adelantado que mi pregunta es más sobre las decisiones que se toman al diseñar el lenguaje, no sobre cómo funciona.
Por lo tanto, considere este código. La función interna es un cierre de python, y var
no es local para outer
, se almacena en una celda (y luego se recupera de una celda):
def outer(): var = 1 def inner(): return var return inner
El desassembly se ve así:
0 LOAD_CONST 1 (1) 3 STORE_DEREF 0 (var) # not STORE_FAST 6 LOAD_CLOSURE 0 (var) 9 BUILD_TUPLE 1 12 LOAD_CONST 2 (<code object inner at 0x10796c810) 15 LOAD_CONST 3 ('outer..inner') 18 MAKE_CLOSURE 0 21 STORE_FAST 0 (inner) 24 LOAD_FAST 0 (inner) 27 RETURN_VALUE recursing into <code object inner at 0x10796c810: 0 LOAD_DEREF 0 (var) # same thing 3 RETURN_VALUE
Esto cambia cuando intentamos vincular otra cosa a var
dentro de la función interna:
def outer(): var = 1 def inner(): var = 2 return var return inner
Una vez más el desassembly:
0 LOAD_CONST 1 (1) 3 STORE_FAST 0 (var) # this one changed 6 LOAD_CONST 2 (<code object inner at 0x1084a1810) 9 LOAD_CONST 3 ('outer..inner') 12 MAKE_FUNCTION 0 # AND not MAKE_CLOSURE 15 STORE_FAST 1 (inner) 18 LOAD_FAST 1 (inner) 21 RETURN_VALUE recursing into <code object inner at 0x1084a1810: 0 LOAD_CONST 1 (2) 3 STORE_FAST 0 (var) # 'var' is supposed to be local 6 LOAD_FAST 0 (var) 9 RETURN_VALUE
Almacenamos var
localmente, lo que cumple con lo que se dice en la documentación: las asignaciones a los nombres siempre entran en el ámbito más interno .
Ahora, cuando intentamos hacer un incremento var += 1
, aparece un LOAD_FAST
desagradable, que intenta obtener var
desde el scope local del inner
:
14 LOAD_FAST 0 (var) 17 LOAD_CONST 2 (2) 20 INPLACE_ADD 21 STORE_FAST 0 (var)
Y por supuesto nos sale un error. Ahora, esto es lo que no entiendo : ¿por qué no podemos recuperar var
con un LOAD_DEREF
, y luego lo almacenamos dentro inner
scope inner
con un STORE_FAST
? Quiero decir, esto parece estar bien con las tareas de “scope más interno”, y al mismo tiempo es algo más intuitivamente deseable. Al menos el código +=
haría lo que queremos que haga, y no se me ocurre una situación en la que el enfoque descrito pueda arruinar algo.
¿Puedes? Siento que me estoy perdiendo algo aquí.
Python tiene una regla muy simple que asigna cada nombre en un scope a exactamente una categoría: local, envolvente o global / incorporada.
(CPython, por supuesto, implementa esa regla mediante el uso de locales FAST, celdas de cierre DEREF y búsquedas de NOMBRE o GLOBAL).
Su regla modificada tiene sentido para su caso simple, pero es fácil encontrar casos en los que sería ambiguo (al menos para un lector humano, si no para el comstackdor). Por ejemplo:
def outer(): var = 1 def inner(): if spam: var = 1 var += 1 return var return inner
¿ LOAD_DEREF
var += 1
hace un LOAD_DEREF
o LOAD_FAST
? No podemos saberlo hasta que sepamos el valor del spam
en el tiempo de ejecución. Lo que significa que no podemos comstackr el cuerpo de la función.
Incluso si pudiera llegar a una regla más complicada que tenga sentido, hay una virtud inherente en que la regla es simple. Además de ser fácil de implementar (y, por lo tanto, fácil de depurar, optimizar, etc.), es fácil de entender para alguien. Cuando obtiene un UnboundLocalError
, cualquier progtwigdor de Python de nivel intermedio sabe cómo trabajar con la regla en su cabeza y averiguar qué fue lo que salió mal.
Mientras tanto, observe que cuando esto aparece en el código de la vida real, hay maneras muy fáciles de solucionarlo explícitamente. Por ejemplo:
def inner(): lvar = var + 1 return lvar
Quería cargar la variable de cierre y asignarla a una variable local. No hay razón para que tengan el mismo nombre. De hecho, usar el mismo nombre es engañoso, incluso con su nueva regla: implica al lector que está modificando la variable de cierre, cuando en realidad no lo está. Así que simplemente dales nombres diferentes, y el problema desaparece.
Y eso todavía funciona con la asignación no local:
def inner(): nonlocal var if spam: var = 1 lvar = var + 1 return lvar
O, por supuesto, hay trucos como el uso de un valor predeterminado de parámetro para crear un local que comienza con una copia de la variable de cierre:
def inner(var=var): var += 1 return var
¿Lo estás haciendo demasiado difícil? var
no puede ser local porque se está anulando la referencia antes de la asignación, y no puede ser no local (a menos que se declare global
o nonlocal
) porque se está asignando a.
El lenguaje está diseñado de esta manera para que (a) no pise accidentalmente las variables globales: la asignación a una variable la hace local, a menos que la declare explícitamente global
o nonlocal
. Y (b) puede usar fácilmente los valores de las variables en los ámbitos externos. Si no hace referencia a un nombre que no ha definido localmente, lo buscará en los ámbitos de encierro.
Su código debe eliminar la referencia a la variable antes de poder incrementarla, por lo que las reglas del lenguaje hacen que la variable sea local y no local, una contradicción. El resultado: su código solo se ejecutará si declara que var
nonlocal
es nonlocal
.
Estás cavando demasiado profundo. Este es un problema de la semántica del lenguaje, no de los códigos de operación y las celdas. inner
contiene una asignación al nombre var
:
def inner(): var += 1 # here return(var)
por lo tanto, según el modelo de ejecución de Python, inner
tiene una variable local llamada var
, y todos los bashs de leer y escribir el nombre var
dentro de inner
usan la variable local. Si bien Python podría haber sido diseñado de modo que si la var
local no está vinculada, intenta la var
del cierre, Python no se diseñó de esa manera.