Python: LOAD_FAST vs. LOAD_DEREF con adición in situ

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.