UnboundLocalError en la variable local cuando se reasigna después del primer uso


209

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 descomento la línea (B) , obtengo una UnboundLocalError: 'c' not assignedlínea en (A) . Los valores de ay bse imprimen correctamente. Esto me tiene completamente desconcertado por dos razones:

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

  2. ¿Por qué son las variables ay los bimpresos como se esperaba, mientras que cplantea un error?

La única explicación que se me ocurre es que una variable localc la asignación creac+=1 , que tiene prioridad sobre la variable "global" cincluso 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 explicarme este comportamiento?


Respuestas:


216

Python trata las variables en funciones de manera diferente dependiendo de si les asigna valores desde dentro o fuera de la función. Si se asigna una variable dentro de una función, se trata por defecto como una variable local. Por lo tanto, cuando descomenta la línea, intenta hacer referencia a la variable local cantes de que se le haya asignado ningún valor.

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

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 ámbito de función de cierre más cercano que tiene una cvariable.


3
Gracias. Pregunta rápida. ¿Esto implica que Python decide el alcance de cada variable antes de ejecutar un programa? Antes de ejecutar una función?
tba

77
La decisión de alcance variable la toma el compilador, que normalmente se ejecuta una vez cuando inicia el programa por primera vez. Sin embargo, vale la pena tener en cuenta que el compilador también podría ejecutarse más tarde si tiene declaraciones "eval" o "exec" en su programa.
Greg Hewgill

2
OK gracias. Supongo que "lenguaje interpretado" no implica tanto como había pensado.
tba

1
Ah, esa palabra clave 'no local' era exactamente lo que estaba buscando, parecía que a Python le faltaba esto. ¿Presumiblemente esto 'cae en cascada' a través de cada alcance que importa la variable usando esta palabra clave?
Brendan

66
@brainfsck: es más fácil de entender si hace la distinción entre "mirar hacia arriba" y "asignar" una variable. La búsqueda vuelve a un ámbito superior si el nombre no se encuentra en el ámbito actual. La asignación siempre se realiza en el ámbito local (a menos que use globalo nonlocalpara forzar una asignación global o no local)
Steven

71

Python es un poco extraño ya que mantiene 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 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 "aha, 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 arroja el error.

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

La razón por la que dice que hay un problema en la línea es porque está buscando efectivamente 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, 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 parsing.

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

c+=1

lo divide en algo como

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

El analizador finalmente quiere convertir esto en un árbol de análisis y ejecutarlo, pero como es una tarea, antes de que lo haga, 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 extra para facilitar la vida más adelante. Solo, luego ve al OPERADOR, ve que las reglas dicen "si tienes un operador + = el lado izquierdo debe haber sido inicializado" y dice "¡vaya!"

El punto aquí es que todavía no ha comenzado realmente el análisis de la línea . Todo esto está sucediendo como una preparación 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, 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.


1
Ok, gracias por tu respuesta; Me aclaró algunas cosas sobre los ámbitos en Python. Sin embargo, todavía no entiendo por qué el error se genera en la línea (A) en lugar de en la línea (B). ¿Python crea su diccionario de alcance variable ANTES de ejecutar el programa?
tba

1
No, está en el nivel de expresión. Agregaré a la respuesta, no creo que pueda encajar esto en un comentario.
Charlie Martin

2
Nota sobre los detalles de implementación: en CPython, el ámbito local generalmente no se maneja como un dict, es internamente solo una matriz ( locals()se rellenará a dictpara regresar, pero los cambios no crean nuevos locals). La fase de análisis es encontrar cada asignación a un local y convertir de nombre a posición en esa matriz, y usar esa posición siempre que se haga referencia al nombre. Al ingresar a la función, los locales sin argumentos se inicializan en un marcador de posición, y UnboundLocalErrors ocurren cuando se lee una variable y su índice asociado todavía tiene el valor del marcador de posición.
ShadowRanger

44

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 se puede ver, el código de bytes para acceder a es LOAD_FAST, y por 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 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 como para los globales. Debido a esto, Python está leyendo la print alínea como "obtener el valor de la variable local 'a' en el espacio 0 e imprimirlo", y cuando detecta que esta variable aún no se ha inicializado, genera una excepción.


10

Python tiene un comportamiento bastante interesante cuando prueba la semántica de variable global tradicional. 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 globalpalabra clave. Intenta 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 obtiene 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á tratando de crear una nueva variable en este ámbito llamada cy modificarla en una sola operación, lo que no está permitido en Python porque esta nueva cno se inicializó.


Gracias por su respuesta, pero no creo que explique por qué el error se arroja en la línea (A), donde simplemente intento imprimir una variable. El programa nunca llega a la línea (B) donde está tratando de modificar una variable no inicializada.
tba

1
Python leerá, analizará y convertirá toda la función en bytecode interno antes de que comience a ejecutar el programa, por lo que el hecho de que "gire c a la variable local" ocurra textualmente después de la impresión del valor no importa, por así decirlo.
Vatine

6

El mejor ejemplo que lo deja claro es:

bar = 42
def foo():
    print bar
    if False:
        bar = 0

cuando se llama foo(), esto también aumenta UnboundLocalError aunque nunca llegaremos a la línea bar=0, por lo que lógicamente nunca se debe crear una variable local.

El misterio radica en " Python es un lenguaje interpretado " y la declaración de la función foose interpreta como una sola declaración (es decir, una declaración compuesta), simplemente la interpreta tontamente y crea ámbitos locales y globales. Por barlo tanto, se reconoce en el ámbito local antes de la ejecución.

Para obtener más ejemplos como este, lea 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:


5

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 ayudarlo a reescribir 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)

4

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

En la mayoría de los casos, tiende a pensar en la asignación aumentada ( a += b) como exactamente equivalente a la asignación simple ( a = a + b). Sin embargo, es posible tener algunos problemas con esto, en un caso de esquina. Dejame explicar:

La forma en que funciona la asignación simple de Python significa que si ase pasa a una función (como func(a); tenga en cuenta que Python siempre se pasa por referencia), entonces a = a + bno modificará lo aque se pasa. En su lugar, solo modificará el puntero local a.

Pero si lo 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 ano se declare global), no hay efectos secundarios fuera del ámbito local, como la asignación aa es solo una actualización del puntero.

En el segundo caso, ase modificará a sí mismo, por lo que todas las referencias a aapuntará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 (trato de usarlo solo para variables locales / de bucle). Utilice una asignación simple y estará a salvo de comportamientos ambiguos.


2

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

Como estoy seguro de que ya sabía, cualquier nombre utilizado a la izquierda de un '=' es implícitamente una variable local. Más de una vez me ha sorprendido cambiando un acceso variable a 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.


2

c+=1 asigna c , python supone que las variables asignadas son locales, pero en este caso no se ha declarado localmente.

O usa el globalononlocal palabras clave .

nonlocal funciona solo en python 3, por lo que si está usando python 2 y no desea que su variable sea global, puede usar un objeto mutable:

my_variables = { # a mutable object
    'c': 3
}

def test():
    my_variables['c'] +=1

test()

1

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

class Employee:
    counter=0

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

0

En python tenemos una declaración similar para todo tipo de variables locales, variables de clase y variables globales. cuando refiere la variable global del método, Python piensa que en realidad está refiriendo la variable del método en sí que aún no está definida y, por lo tanto, arroja un error. Para referirnos a la variable global tenemos que usar globals () ['variableName'].

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


0

El mismo problema me molesta. Usando nonlocaly globalpuede resolver el problema.
Sin embargo, la atención necesaria para el uso de nonlocal, funciona para funciones anidadas. Sin embargo, en un nivel de módulo, no funciona. Ver ejemplos aquí.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.