UnboundLocalError al intentar utilizar una variable (supuestamente global) que está (re)asignada (incluso después del primer uso)

Resuelto tba asked hace 15 años • 14 respuestas

Cuando pruebo este código:

a, b, c = (1, 2, 3)

def test():
    print(a)
    print(b)
    print(c)
    c += 1
test()

Recibo un error en la print(c)línea que dice:

UnboundLocalError: local variable 'c' referenced before assignment

en versiones más recientes de Python, o

UnboundLocalError: 'c' not assigned

en algunas versiones anteriores.

Si comento c += 1, ambos printtienen éxito.

No entiendo: ¿por qué la impresión afunciona b, si cno? ¿Cómo se c += 1produjo print(c)el error, incluso cuando aparece más adelante en el código?

Parece que la asignación c += 1crea una variable localc , que tiene prioridad sobre la global c. Pero, ¿cómo puede una variable "robar" alcance antes de existir? c¿Por qué aparentemente es local aquí?


Consulte también Uso de variables globales en una función para preguntas que tratan simplemente sobre cómo reasignar una variable global desde dentro de una función y ¿Es posible modificar una variable en Python que se encuentra en un alcance externo (cerrado), pero no global? para reasignar desde una función envolvente (cierre).

Consulte ¿Por qué no se necesita la palabra clave "global" para acceder a una variable global? para los casos en los que OP esperaba un error pero no obtuvo uno, simplemente accediendo a un global sin la globalpalabra clave.

Consulte ¿Cómo se puede "desvincular" un nombre en Python? ¿Qué código puede causar un "UnboundLocalError"? para los casos en los que OP esperaba que la variable fuera local, pero tiene un error lógico que impide la asignación en todos los casos.

tba avatar Dec 16 '08 10:12 tba
Aceptado

Python trata las variables en funciones de manera diferente dependiendo de si les asigna valores desde dentro o fuera de la función. Si una variable se asigna dentro de una función, se trata de forma predeterminada como una variable local. Por lo tanto, cuando descomentas la línea, estás intentando hacer referencia a la variable local cantes de que se le haya asignado cualquier valor.

Si desea que la variable cse refiera al global c = 3asignado antes de la función, coloque

global c

como primera línea de la función.

En cuanto a Python 3, ahora existe

nonlocal c

que puede usar para referirse al alcance de la función adjunta más cercana que tiene una cvariable.

recursive avatar Dec 16 '2008 03:12 recursive

Python es un poco extraño porque mantiene todo en un diccionario para los distintos ámbitos. Los a,b,c originales están en el alcance superior y, por lo tanto, en ese diccionario superior. La función tiene su propio diccionario. Cuando llega a las declaraciones print(a)y print(b), no hay nada con ese nombre en el diccionario, por lo que Python busca la lista y las 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 "ajá, hay una variable llamada c, la pondré en mi diccionario de alcance local". Luego, cuando busca un valor para c para c en el lado derecho de la asignación, encuentra su variable local llamada c , que aún no tiene valor, y por lo tanto arroja el error.

La declaración global cmencionada anteriormente simplemente le dice al analizador que usa el calcance global y por lo tanto no necesita uno nuevo.

La razón por la que dice que hay un problema en la línea que hace es porque efectivamente está buscando los nombres antes de intentar generar código, por lo que 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 demasiado en serio los mensajes de un compilador.

Si te sirve de consuelo, probablemente pasé un día investigando y experimentando con este mismo tema antes de encontrar algo que Guido había escrito sobre los diccionarios que lo explicaban todo.

Actualización, ver comentarios:

No escanea el código dos veces, pero sí escanea el código en dos fases, lectura y análisis.

Considere 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 llegue a la línea

c+=1

lo divide en algo como

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

El analizador eventualmente quiere convertir esto en un árbol de análisis y ejecutarlo, pero como es una tarea, antes de hacerlo, busca el nombre c en el diccionario local, no lo ve y lo inserta en el diccionario, marcando como no inicializado. En un lenguaje completamente compilado, simplemente iría a la tabla de símbolos y esperaría el análisis, pero como NO tendrá el lujo de una segunda pasada, el lexer hace un poco de trabajo adicional para hacer la vida más fácil más adelante. Sólo que entonces ve al OPERADOR, ve que las reglas dicen "si tiene un operador += el lado izquierdo debe haber sido inicializado" y dice "¡vaya!"

El punto aquí es que todavía no ha comenzado el análisis de la línea . Todo esto está sucediendo como 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 compiladores son más honestos al respecto y dicen "error en o alrededor de la línea XXX", pero este no es así.

Charlie Martin avatar Dec 16 '2008 03:12 Charlie Martin

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 código de bytes para acceder a a es LOAD_FASTy para b LOAD_GLOBAL,. Esto se debe a que el compilador 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: se les asigna estáticamente un desplazamiento 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 dict más costosa como para los globales. Debido a esto, Python lee la print alínea como "obtener el valor de la variable local 'a' contenida en la ranura 0 e imprimirlo", y cuando detecta que esta variable aún no está inicializada, genera una excepción.

Brian avatar Dec 16 '2008 09:12 Brian

Python tiene un comportamiento bastante interesante cuando se prueba la semántica de variables globales tradicional. No recuerdo los detalles, pero puedes leer bien el valor de una variable declarada en el ámbito 'global', pero si quieres modificarlo, debes usar la globalpalabra clave. Intente cambiar 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 estaría completamente separada. El intérprete cree que está intentando crear una nueva variable en este alcance cy modificarla todo en una sola operación, lo cual no está permitido en Python porque esta nueva cno se inicializó.

Mongoose avatar Dec 16 '2008 03:12 Mongoose