Error de scope de la variable Python

El siguiente código funciona como se espera 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 descomento la línea (B) , obtengo un UnboundLocalError: 'c' not assigned en la línea (A) . Los valores de a y b se imprimen correctamente. Esto me ha desconcertado completamente por dos razones:

  1. ¿Por qué se produce un error de tiempo de ejecución en la línea (A) debido a una statement posterior en la línea (B) ?

  2. ¿Por qué las variables a y b imprimen como se espera, mientras que c genera un error?

La única explicación que se me ocurre es que la variable c c+=1 crea una variable local c , que tiene prioridad sobre la variable c “global” incluso antes de que se cree la variable local. Por supuesto, no tiene sentido que una variable “robe” el scope antes de que exista.

¿Podría alguien explicar este comportamiento?

Python trata las variables en las funciones de manera diferente dependiendo de si les asigna valores desde dentro de la función o no. Si una función contiene alguna asignación a una variable, se trata por defecto como una variable local. Por lo tanto, cuando elimine el comentario de la línea, está intentando 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 primera línea de la función.

En cuanto a Python 3, ahora hay

 nonlocal c 

que puede usar para referirse al scope de la función de cierre más cercano que tiene una variable c .

Python es un poco raro ya que guarda todo en un diccionario para los diversos ámbitos. Los originales a, b, c están en el ámbito superior y, por lo tanto, en ese diccionario superior. La función tiene su propio diccionario. Cuando llega a los estados de print(a) e print(b) , no hay nada con ese nombre en el diccionario, por lo que Python busca en la lista y los 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 scope local”. Luego, cuando va en busca de un valor para c para la c en el lado derecho de la asignación, encuentra su variable local llamada c , que aún no tiene un valor, y así lanza el error.

La statement global c mencionada anteriormente simplemente le dice al analizador que usa la c del ámbito global y por lo tanto no necesita una nueva.

La razón por la que dice que hay un problema en la línea que lo hace es porque está buscando efectivamente los nombres antes de que intente generar código, por lo que, en cierto sentido, aún no cree que realmente esté haciendo esa línea. Yo diría que es un error de usabilidad, pero en general es una buena práctica aprender a no tomar los mensajes de un comstackdor demasiado en serio.

Si me sirve de consuelo, probablemente pasé 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 sí escanea el código en dos fases, analizando y analizando.

Considera cómo funciona el análisis de esta línea de código. El lexer lee el texto de origen y lo divide en lexemas, los “componentes más pequeños” de la gramática. Así que cuando llega a la línea

 c+=1 

se rompe en algo como

 SYMBOL(c) OPERATOR(+=) DIGIT(1) 

El analizador eventualmente desea convertir esto en un árbol de análisis y ejecutarlo, pero como es una asignación, antes de que lo haga, 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 comstackdo, solo iría a la tabla de símbolos y esperaría el análisis, pero dado que NO tendrá el lujo de una segunda pasada, el lexer hace un poco más de trabajo para facilitar la vida más adelante. Solo, luego ve al OPERADOR, ve que las reglas dicen “si tiene un operador + = el lado izquierdo debe haberse inicializado” y dice “¡Vaya!”

El punto aquí es que aún no ha comenzado el análisis de la línea . Todo esto está sucediendo en una especie de preparación para el análisis real, por lo que el contador de líneas 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 comstackdores son más honestos al respecto y dicen “error en o alrededor de la línea XXX”, pero este no lo hace.

Mirar el desassembly 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 LOAD_FAST acceso para acceder a es LOAD_FAST , y para b, LOAD_GLOBAL . Esto se debe a que el comstackdor ha identificado que a está asignado dentro de la función y lo ha clasificado como una variable local. El mecanismo de acceso para los locales es fundamentalmente diferente para los globales: a ellos se les asigna un desplazamiento estático 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 está leyendo la línea de print a como “obtener el valor de la variable local ‘a’ mantenida en la ranura 0, e imprimirla”, y cuando detecta que esta variable aún no está inicializada, genera una excepción.

Python tiene un comportamiento bastante interesante al probar la semántica de variables globales tradicionales. No recuerdo los detalles, pero puede leer bien el valor de una variable declarada en el ámbito ‘global’, pero si desea modificarla, debe usar la palabra clave global . Intenta 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 está recibiendo 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 cree que está intentando crear 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ó.

Aquí hay dos enlaces que pueden ayudar

1: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value

2: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#how-do-i-write-a-function-with-output-parameters-call-by-reference

El enlace uno describe el error UnboundLocalError. El enlace dos puede ayudar a reescribir la 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 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 genera UnboundLocalError aunque nunca alcanzaremos la bar=0 línea bar=0 , por lo que la variable local lógica no debería crearse nunca.

El misterio se encuentra en ” Python es un lenguaje interpretado ” y la statement de la función foo se interpreta como una sola statement (es decir, una statement compuesta), simplemente la interpreta de manera confusa y crea ámbitos locales y globales. Así que la bar es reconocida en el ámbito local antes de la ejecución.

Para obtener más ejemplos como este, lea este post: 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 scope de las variables de Python:

Esta no es una respuesta directa a su pregunta, pero está estrechamente relacionada, ya que es otra consecuencia de la relación entre la asignación aumentada y los ámbitos de función.

En la mayoría de los casos, tiende a pensar que la asignación aumentada ( a += b ) es exactamente equivalente a la asignación simple ( a = a + b ). Es posible tener algún problema con esto, sin embargo, 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 se pasa por referencia), entonces a = a + b no modificará la a que se pasa. En su lugar, solo modificará el puntero local a.

Pero si usa a += b , a veces se implementa como:

 a = a + b 

O a veces (si existe el método) como:

 a.__iadd__(b) 

En el primer caso (siempre y cuando no se declare global), no hay efectos secundarios fuera del scope local, ya que la asignación a a es solo una actualización de puntero.

En el segundo caso, a se modificará realmente, por lo que todas las referencias a a apuntarán a la versión modificada. Esto se demuestra mediante 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 

Así que el truco es evitar la asignación aumentada en los argumentos de la función (trato de usarla solo para variables locales / de bucle). Use una tarea simple y estará a salvo de un comportamiento ambiguo.

El intérprete de Python leerá una función como una unidad completa. Pienso que es como leerlo en dos pases, una vez para reunir su cierre (las variables locales), y luego otra vez para convertirlo en un código de bytes.

Como estoy seguro de que ya sabías, cualquier nombre que se use a la izquierda de ‘=’ es implícitamente una variable local. Más de una vez me sorprendió cambiando el acceso de una 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 scope global. Obtienes el mismo comportamiento con funciones anidadas.

c+=1 asigna c , python asume 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, así que si estás usando python 2 y no quieres que tu variable sea global, puedes usar un objeto mutable:

 my_variables = { # a mutable object 'c': 3 } def test(): my_variables['c'] +=1 test() 

La mejor manera de alcanzar la variable de clase es acceder directamente por nombre de clase

 class Employee: counter=0 def __init__(self): Employee.counter+=1 

En Python tenemos una statement similar para todo tipo de variables locales, variables de clase y variables globales. cuando se refiere la variable global desde el método, Python piensa que en realidad está refiriendo la variable desde el propio método que aún no está definido y, por lo tanto, produce un error. Para referir una variable global tenemos que usar globals () [‘variableName’].

en su caso use globals () [‘a], globals () [‘ b ‘] y globals () [‘ c ‘] en lugar de a, byc respectivamente.