unboundlocalerror - Error de alcance variable de Python
variable local python (10)
Aquí hay dos enlaces que pueden ayudar
el enlace uno describe el error UnboundLocalError. El enlace dos puede ayudar con la reescritura de su función de prueba. Basado en el enlace dos, el problema original podría reescribirse como:
>>> a, b, c = (1, 2, 3)
>>> print (a, b, c)
(1, 2, 3)
>>> def test (a, b, c):
... print (a)
... print (b)
... print (c)
... c += 1
... return a, b, c
...
>>> a, b, c = test (a, b, c)
1
2
3
>>> print (a, b ,c)
(1, 2, 4)
El siguiente código funciona como se esperaba en Python 2.5 y 3.0:
a, b, c = (1, 2, 3)
print(a, b, c)
def test():
print(a)
print(b)
print(c) # (A)
#c+=1 # (B)
test()
Sin embargo, cuando descomiento la línea (B) , obtengo un UnboundLocalError: ''c'' not assigned
en la línea (A) . Los valores de b
están impresos correctamente. Esto me tiene completamente desconcertado por dos razones:
¿Por qué se lanza un error de tiempo de ejecución en la línea (A) debido a una declaración posterior en la línea (B) ?
¿Por qué se imprimen las variables
a
yb
como se espera, mientras quec
genera un error?
La única explicación que se me ocurre es que una variable local c
se crea mediante la asignación c+=1
, que tiene precedencia sobre la variable "global" c
incluso antes de que se cree la variable local. Por supuesto, no tiene sentido que una variable "robe" el alcance antes de que exista.
¿Podría alguien explicar este comportamiento?
Echar un vistazo al desmontaje puede aclarar lo que está sucediendo:
>>> def f():
... print a
... print b
... a = 1
>>> import dis
>>> dis.dis(f)
2 0 LOAD_FAST 0 (a)
3 PRINT_ITEM
4 PRINT_NEWLINE
3 5 LOAD_GLOBAL 0 (b)
8 PRINT_ITEM
9 PRINT_NEWLINE
4 10 LOAD_CONST 1 (1)
13 STORE_FAST 0 (a)
16 LOAD_CONST 0 (None)
19 RETURN_VALUE
Como puede ver, el bytecode para acceder a a es LOAD_FAST
, y para b, LOAD_GLOBAL
. Esto se debe a que el compilador ha identificado que a está asignado dentro de la función y lo clasificó como una variable local. El mecanismo de acceso para los lugareños es fundamentalmente diferente para los globales: se les asigna estáticamente una compensación en la tabla de variables del marco, lo que significa que la búsqueda es un índice rápido, en lugar de la búsqueda de dict más costosa que para los globales. Debido a esto, Python lee la línea de print a
como "obtener el valor de la variable local ''a'' retenida en la ranura 0 e imprimirla", y cuando detecta que esta variable aún no está inicializada, genera una excepción.
El intérprete de Python leerá una función como una unidad completa. Pienso en ello como si lo leyera en dos pasadas, una para reunir su cierre (las variables locales) y otra vez para convertirlo en código de bytes.
Como estoy seguro de que ya sabía, cualquier nombre usado a la izquierda de ''='' es implícitamente una variable local. Más de una vez me han pillado cambiando un acceso variable a + = y de repente es una variable diferente.
También quería señalar que no tiene nada que ver específicamente con el alcance global. Obtiene el mismo comportamiento con funciones anidadas.
El mejor ejemplo que lo deja en claro es:
bar = 42
def foo():
print bar
if False:
bar = 0
cuando se llama a foo()
, esto también aumenta UnboundLocalError
aunque nunca llegaremos a la bar=0
líneas bar=0
, por lo que lógicamente la variable local nunca se debe crear.
El misterio radica en " Python es un lenguaje interpretado " y la declaración de la función foo
se interpreta como una declaración única (es decir, una declaración compuesta), simplemente la interpreta tontamente y crea ámbitos locales y globales. Entonces, la bar
se reconoce en el alcance local antes de la ejecución.
Para ver más ejemplos como este, lee esta publicación: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/
Esta publicación proporciona una descripción completa y análisis del alcance de Python de variables:
Esta no es una respuesta directa a su pregunta, pero está estrechamente relacionada, ya que es otra complicación causada por la relación entre la asignación aumentada y los ámbitos funcionales.
En la mayoría de los casos, se tiende a pensar que la asignación aumentada ( a += b
) es exactamente equivalente a la asignación simple ( a = a + b
). Sin embargo, es posible meterse en problemas con esto, en un caso de esquina. Dejame explicar:
La forma en que funciona la asignación simple de Python significa que si a
se pasa a una función (como func(a)
; tenga en cuenta que Python siempre pasa por referencia), entonces a = a + b
no modificará la a
que se pasa. En su lugar, solo modificará el puntero local a a
.
Pero si usa a += b
, a veces se implementa como:
a = a + b
o a veces (si el método existe) como:
a.__iadd__(b)
En el primer caso (siempre que a
no se declare global), no hay efectos secundarios fuera del alcance local, ya que la asignación a a
es solo una actualización del puntero.
En el segundo caso, a
voluntad se modificará a sí misma, por lo que todas las referencias a a
apuntarán a la versión modificada. Esto se demuestra con el siguiente código:
def copy_on_write(a):
a = a + a
def inplace_add(a):
a += a
a = [1]
copy_on_write(a)
print a # [1]
inplace_add(a)
print a # [1, 1]
b = 1
copy_on_write(b)
print b # [1]
inplace_add(b)
print b # 1
Entonces, el truco es evitar la asignación aumentada en los argumentos de la función (intento usarlo solo para variables locales / de bucle). Use asignaciones simples, y estará a salvo del comportamiento ambiguo.
La mejor forma de llegar a la variable de clase es acceder directamente por nombre de clase
class Employee:
counter=0
def __init__(self):
Employee.counter+=1
Python es un poco extraño ya que mantiene todo en un diccionario para varios ámbitos. Los originales a, b, c están en el ámbito superior y, por lo tanto, en el diccionario más superior. La función tiene su propio diccionario. Cuando llegue a las instrucciones print(a)
e print(b)
, no hay nada con ese nombre en el diccionario, por lo que Python busca la lista y la encuentra en el diccionario global.
Ahora llegamos a c+=1
, que es, por supuesto, equivalente a c=c+1
. Cuando Python escanea esa línea, dice "aha, hay una variable llamada c, la pondré en mi diccionario de alcance local". Luego, cuando busca un valor para c para la c en el lado derecho de la asignación, encuentra su variable local llamada c , que todavía no tiene valor, y arroja el error.
La sentencia global c
mencionada anteriormente simplemente le dice al analizador que usa la c
del alcance global y por lo tanto no necesita una nueva.
La razón por la que dice que hay un problema en la línea es porque efectivamente busca los nombres antes de que intente generar código, y por lo tanto, en cierto sentido, no cree que realmente esté haciendo esa línea todavía. Yo diría que es un error de usabilidad, pero generalmente es una buena práctica aprender a no tomar los mensajes de un compilador demasiado en serio.
Si te sirve de consuelo, pasé probablemente un día cavando y experimentando con este mismo problema antes de encontrar algo que Guido había escrito sobre los diccionarios que explicaban todo.
Actualización, ver comentarios:
No escanea el código dos veces, pero escanea el código en dos fases, lexing y análisis.
Considera cómo funciona el análisis de esta línea de código. El lexer lee el texto fuente y lo divide en lexemas, los "componentes más pequeños" de la gramática. Entonces cuando golpea la línea
c+=1
lo divide en algo así como
SYMBOL(c) OPERATOR(+=) DIGIT(1)
El analizador finalmente quiere convertirlo en un árbol de análisis y ejecutarlo, pero dado que se trata de una tarea, antes de hacerlo, busca el nombre c en el diccionario local, no lo ve y lo inserta en el diccionario, marcando como sin inicializar. En un lenguaje completamente compilado, simplemente irá a la tabla de símbolos y esperará el análisis, pero como NO tendrá el lujo de un segundo pase, el lexer hará un poco más de trabajo para facilitar la vida más adelante. Solo cuando ve al OPERADOR, ve que las reglas dicen "si tiene un operador + = el lado izquierdo debe haberse inicializado" y dice "¡Ups!"
El punto aquí es que todavía no ha comenzado el análisis de la línea . Todo esto está sucediendo como algo preparatorio para el análisis real, por lo que el contador de línea no ha avanzado a la siguiente línea. Por lo tanto, cuando señala el error, todavía piensa que está en la línea anterior.
Como digo, se podría argumentar que es un error de usabilidad, pero en realidad es algo bastante común. Algunos compiladores son más honestos y dicen "error en o alrededor de la línea XXX", pero este no.
Python tiene un comportamiento bastante interesante cuando intentas la semántica de variables globales tradicionales. No recuerdo los detalles, pero puede leer el valor de una variable declarada en el alcance ''global'' muy bien, pero si desea modificarlo, debe usar la palabra clave global
. Pruebe a cambiar la test()
a esto:
def test():
global c
print(a)
print(b)
print(c) # (A)
c+=1 # (B)
Además, la razón por la que recibe este error es porque también puede declarar una nueva variable dentro de esa función con el mismo nombre que una ''global'', y sería completamente independiente. El intérprete piensa que está intentando hacer una nueva variable en este ámbito llamada c
y modificarla en una sola operación, lo que no está permitido en Python porque esta nueva c
no se inicializó.
Python trata las variables en funciones de manera diferente dependiendo de si le asigna valores desde dentro de la función o no. Si una función contiene asignaciones a una variable, se trata de manera predeterminada como una variable local. Por lo tanto, cuando descomenta la línea, intenta hacer referencia a una variable local antes de que se le haya asignado algún valor.
Si desea que la variable c
refiera a la c
global
global c
como la primera línea de la función.
En cuanto a Python 3, ahora hay
nonlocal c
que puede usar para referirse al alcance de la función circundante más cercana que tiene una variable c
.
c+=1
asigna c
, python supone que las variables asignadas son locales, pero en este caso no se ha declarado localmente.
Utilice las palabras clave global
o nonlocal
.
nonlocal
solo funciona en python 3, por lo que si está utilizando python 2 y no quiere que su variable sea global, puede usar un objeto mutable:
my_variables = { # a mutable object
''c'': 3
}
def test():
my_variables[''c''] +=1
test()