Accediendo a las variables de clase desde una lista de comprensión en la definición de clase.

¿Cómo accede a otras variables de clase desde una lista de comprensión dentro de la definición de clase? Lo siguiente funciona en Python 2, pero falla en Python 3:

class Foo: x = 5 y = [x for i in range(1)] 

Python 3.2 da el error:

 NameError: global name 'x' is not defined 

Intentar Foo.x tampoco funciona. ¿Alguna idea sobre cómo hacer esto en Python 3?

Un ejemplo de motivación un poco más complicado:

 from collections import namedtuple class StateDatabase: State = namedtuple('State', ['name', 'capital']) db = [State(*args) for args in [ ['Alabama', 'Montgomery'], ['Alaska', 'Juneau'], # ... ]] 

En este ejemplo, apply() hubiera sido una solución decente, pero lamentablemente se eliminó de Python 3.

El scope de la clase y la lista, las definiciones de conjuntos o diccionarios, así como las expresiones del generador no se mezclan.

El porque; o, la palabra oficial en este

En Python 3, a las comprensiones de la lista se les asignó un ámbito propio (espacio de nombres local) propio, para evitar que sus variables locales se desborden en el ámbito circundante (consulte la reclasificación de los nombres de Python, incluso después del scope de la comprensión. ¿Esto es correcto? ). Eso es genial cuando se usa una comprensión de esa lista en un módulo o en una función, pero en las clases, el scope es un poco extraño , uhm.

Esto está documentado en pep 227 :

Los nombres en el scope de la clase no son accesibles. Los nombres se resuelven en el ámbito de la función de cierre más interno. Si una definición de clase se produce en una cadena de ámbitos nesteds, el proceso de resolución omite las definiciones de clase.

y en la documentación de la statement compuesta de la class :

La suite de la clase se ejecuta luego en un nuevo marco de ejecución (consulte la sección Nombramiento y enlace ), utilizando un espacio de nombres local recién creado y el espacio de nombres global original. (Por lo general, la suite solo contiene definiciones de funciones). Cuando la suite de la clase termina de ejecutarse, su marco de ejecución se descarta pero su espacio de nombres local se guarda . [4] A continuación, se crea un objeto de clase utilizando la lista de herencia para las clases base y el espacio de nombres local guardado para el diccionario de atributos.

Énfasis mío; El marco de ejecución es el ámbito temporal.

Debido a que el scope se reutiliza como los atributos en un objeto de clase, lo que permite que se use como un scope no local también conduce a un comportamiento indefinido; ¿Qué pasaría si un método de clase se refiriera a x como una variable de scope anidada, y luego manipule Foo.x , por ejemplo? Más importante aún, ¿qué significaría eso para las subclases de Foo ? Python tiene que tratar el scope de una clase de manera diferente, ya que es muy diferente del scope de una función.

Por último, pero definitivamente no menos importante, la sección vinculada de nombre y enlace en la documentación del modelo de ejecución menciona explícitamente los ámbitos de la clase:

El scope de los nombres definidos en un bloque de clase se limita al bloque de clase; no se extiende a los bloques de código de los métodos; esto incluye comprensiones y expresiones generadoras, ya que se implementan utilizando un ámbito de función. Esto significa que lo siguiente fallará:

 class A: a = 42 b = list(a + i for i in range(10)) 

Entonces, para resumir: no puede acceder al scope de la clase desde funciones, listas de comprensión o expresiones generadoras incluidas en ese scope; actúan como si ese scope no existiera. En Python 2, las integraciones de listas se implementaron mediante un acceso directo, pero en Python 3 obtuvieron su propio scope de función (como deberían haber tenido todo el tiempo) y, por lo tanto, su ejemplo se rompe. Otros tipos de comprensión tienen su propio scope, independientemente de la versión de Python, por lo que un ejemplo similar con un conjunto o comprensión de dictado se rompería en Python 2.

 # Same error, in Python 2 or 3 y = {x: x for i in range(1)} 

La (pequeña) excepción; o, ¿por qué una parte puede seguir funcionando?

Hay una parte de una expresión de comprensión o generador que se ejecuta en el ámbito circundante, independientemente de la versión de Python. Esa sería la expresión para el iterable más externo. En tu ejemplo, es el range(1) :

 y = [x for i in range(1)] # ^^^^^^^^ 

Por lo tanto, usar x en esa expresión no generaría un error:

 # Runs fine y = [i for i in range(x)] 

Esto solo se aplica a lo más externo iterable; Si una comprensión tiene varias cláusulas for cláusulas, los iterables for cláusulas internas se evalúan en el scope de la comprensión:

 # NameError y = [i for i in range(1) for j in range(x)] 

Esta decisión de diseño se tomó con el fin de generar un error en el momento de la creación de un genexp en lugar del tiempo de iteración cuando se crea el iterativo más externo de una expresión generadora que produce un error, o cuando el iterable más externo resulta que no es iterable. Las comprensiones comparten este comportamiento para la consistencia.

Mirando bajo el capó; O, mucho más detalle de lo que siempre quisiste.

Puedes ver todo esto en acción usando el módulo dis . Estoy usando Python 3.3 en los siguientes ejemplos, porque agrega nombres calificados que identifican perfectamente los objetos de código que queremos inspeccionar. El bytecode producido es funcionalmente idéntico a Python 3.2.

Para crear una clase, Python esencialmente toma todo el conjunto que conforma el cuerpo de la clase (por lo que todo sangra un nivel más profundo que la class : línea), y lo ejecuta como si fuera una función:

 >>> import dis >>> def foo(): ... class Foo: ... x = 5 ... y = [x for i in range(1)] ... return Foo ... >>> dis.dis(foo) 2 0 LOAD_BUILD_CLASS 1 LOAD_CONST 1 (", line 2>) 4 LOAD_CONST 2 ('Foo') 7 MAKE_FUNCTION 0 10 LOAD_CONST 2 ('Foo') 13 CALL_FUNCTION 2 (2 positional, 0 keyword pair) 16 STORE_FAST 0 (Foo) 5 19 LOAD_FAST 0 (Foo) 22 RETURN_VALUE 

El primer LOAD_CONST carga un objeto de código para el cuerpo de la clase Foo , luego lo convierte en una función y lo llama. El resultado de esa llamada se utiliza para crear el espacio de nombres de la clase, su __dict__ . Hasta ahora tan bueno.

Lo que hay que tener en cuenta aquí es que el código de byte contiene un objeto de código nested; en Python, las definiciones de clase, las funciones, las comprensiones y los generadores están representados como objetos de código que contienen no solo el código de bytes, sino también estructuras que representan variables locales, constantes, variables tomadas de globales y variables tomadas del scope nested. El bytecode comstackdo se refiere a esas estructuras y el intérprete de python sabe cómo acceder a las que se proporcionan a los bytecodes presentados.

Lo importante a recordar aquí es que Python crea estas estructuras en tiempo de comstackción; el conjunto de class es un objeto de código ( ", line 2> ) que ya está comstackdo.

Inspeccionemos ese objeto de código que crea el propio cuerpo de clase; Los objetos de código tienen una estructura de co_consts :

 >>> foo.__code__.co_consts (None, ", line 2>, 'Foo') >>> dis.dis(foo.__code__.co_consts[1]) 2 0 LOAD_FAST 0 (__locals__) 3 STORE_LOCALS 4 LOAD_NAME 0 (__name__) 7 STORE_NAME 1 (__module__) 10 LOAD_CONST 0 ('foo..Foo') 13 STORE_NAME 2 (__qualname__) 3 16 LOAD_CONST 1 (5) 19 STORE_NAME 3 (x) 4 22 LOAD_CONST 2 ( at 0x10a385420, file "", line 4>) 25 LOAD_CONST 3 ('foo..Foo.') 28 MAKE_FUNCTION 0 31 LOAD_NAME 4 (range) 34 LOAD_CONST 4 (1) 37 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 40 GET_ITER 41 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 44 STORE_NAME 5 (y) 47 LOAD_CONST 5 (None) 50 RETURN_VALUE 

El código de bytes anterior crea el cuerpo de la clase. La función se ejecuta y el espacio de nombres locals() resultante, que contiene x e y se usa para crear la clase (excepto que no funciona porque x no está definido como global). Tenga en cuenta que después de almacenar 5 en x , carga otro objeto de código; esa es la lista de comprensión; está envuelto en un objeto de función al igual que el cuerpo de la clase; la función creada toma un argumento posicional, el range(1) puede utilizar para su código de bucle, se convierte en un iterador. Como se muestra en el código de bytes, el range(1) se evalúa en el scope de la clase.

De esto se puede ver que la única diferencia entre un objeto de código para una función o un generador, y un objeto de código para una comprensión es que este último se ejecuta inmediatamente cuando se ejecuta el objeto de código principal; el bytecode simplemente crea una función sobre la marcha y la ejecuta en unos pocos pasos.

Python 2.x usa el bytecode en línea allí, aquí se muestra la salida de Python 2.7:

  2 0 LOAD_NAME 0 (__name__) 3 STORE_NAME 1 (__module__) 3 6 LOAD_CONST 0 (5) 9 STORE_NAME 2 (x) 4 12 BUILD_LIST 0 15 LOAD_NAME 3 (range) 18 LOAD_CONST 1 (1) 21 CALL_FUNCTION 1 24 GET_ITER >> 25 FOR_ITER 12 (to 40) 28 STORE_NAME 4 (i) 31 LOAD_NAME 2 (x) 34 LIST_APPEND 2 37 JUMP_ABSOLUTE 25 >> 40 STORE_NAME 5 (y) 43 LOAD_LOCALS 44 RETURN_VALUE 

No se carga ningún objeto de código, en su lugar se FOR_ITER un bucle FOR_ITER línea. Entonces, en Python 3.x, al generador de listas se le asignó un código propio de objeto, lo que significa que tiene su propio scope.

Sin embargo, la comprensión se compiló junto con el rest del código fuente de Python cuando el intérprete cargó el módulo o la secuencia de comandos, y el comstackdor no considera que un conjunto de clases sea un ámbito válido. Cualquier variable referenciada en una lista de comprensión debe buscar recursivamente en el ámbito que rodea la definición de clase. Si el comstackdor no encontró la variable, la marca como global. El desassembly del objeto de código de comprensión de lista muestra que x se carga como un global:

 >>> foo.__code__.co_consts[1].co_consts ('foo..Foo', 5,  at 0x10a385420, file "", line 4>, 'foo..Foo.', 1, None) >>> dis.dis(foo.__code__.co_consts[1].co_consts[2]) 4 0 BUILD_LIST 0 3 LOAD_FAST 0 (.0) >> 6 FOR_ITER 12 (to 21) 9 STORE_FAST 1 (i) 12 LOAD_GLOBAL 0 (x) 15 LIST_APPEND 2 18 JUMP_ABSOLUTE 6 >> 21 RETURN_VALUE 

Esta parte del código de bytes carga el primer argumento pasado en (el range(1) iterador), y al igual que la versión Python 2.x usa FOR_ITER para realizar un bucle sobre él y crear su salida.

Si hubiésemos definido x en la función foo , x sería una variable de celda (las celdas se refieren a ámbitos nesteds):

 >>> def foo(): ... x = 2 ... class Foo: ... x = 5 ... y = [x for i in range(1)] ... return Foo ... >>> dis.dis(foo.__code__.co_consts[2].co_consts[2]) 5 0 BUILD_LIST 0 3 LOAD_FAST 0 (.0) >> 6 FOR_ITER 12 (to 21) 9 STORE_FAST 1 (i) 12 LOAD_DEREF 0 (x) 15 LIST_APPEND 2 18 JUMP_ABSOLUTE 6 >> 21 RETURN_VALUE 

LOAD_DEREF cargará indirectamente x desde los objetos de celda del objeto de código:

 >>> foo.__code__.co_cellvars # foo function `x` ('x',) >>> foo.__code__.co_consts[2].co_cellvars # Foo class, no cell variables () >>> foo.__code__.co_consts[2].co_consts[2].co_freevars # Refers to `x` in foo ('x',) >>> foo().y [2] 

La referencia real busca el valor hacia arriba desde las estructuras de datos de marco actuales, que se inicializaron desde el atributo .__closure__ de un objeto de .__closure__ . Dado que la función creada para el código de comprensión objeto se vuelve a descartar, no conseguimos inspeccionar el cierre de esa función. Para ver un cierre en acción, tendríamos que inspeccionar una función anidada en su lugar:

 >>> def spam(x): ... def eggs(): ... return x ... return eggs ... >>> spam(1).__code__.co_freevars ('x',) >>> spam(1)() 1 >>> spam(1).__closure__ >>> spam(1).__closure__[0].cell_contents 1 >>> spam(5).__closure__[0].cell_contents 5 

Entonces, para resumir:

  • Las comprensiones de lista obtienen sus propios objetos de código en Python 3, y no hay diferencia entre los objetos de código para funciones, generadores o comprensiones; Los objetos de código de comprensión se envuelven en un objeto de función temporal y se llaman inmediatamente.
  • Los objetos de código se crean en tiempo de comstackción, y las variables no locales se marcan como variables globales o libres, según los ámbitos nesteds del código. El cuerpo de la clase no se considera un ámbito para buscar esas variables.
  • Al ejecutar el código, Python solo tiene que buscar en los elementos globales o el cierre del objeto que se está ejecutando actualmente. Dado que el comstackdor no incluyó el cuerpo de la clase como un scope, el espacio de nombres de la función temporal no se considera.

Una solución; o que hacer al respecto

Si tuviera que crear un scope explícito para la variable x , como en una función, puede usar variables de scope de clase para una lista de comprensión:

 >>> class Foo: ... x = 5 ... def y(x): ... return [x for i in range(1)] ... y = y(x) ... >>> Foo.y [5] 

La función y 'temporal' puede ser llamada directamente; Lo reemplazamos cuando lo hacemos con su valor de retorno. Se considera su scope al resolver x :

 >>> foo.__code__.co_consts[1].co_consts[2] ", line 4> >>> foo.__code__.co_consts[1].co_consts[2].co_cellvars ('x',) 

Por supuesto, las personas que lean su código se enojarán un poco por esto; es posible que desee poner un comentario grande y gordo allí explicando por qué está haciendo esto.

La mejor __init__ es usar __init__ para crear una variable de instancia en su lugar:

 def __init__(self): self.y = [self.x for i in range(1)] 

y evita todos los rasguños y preguntas para explicarte. Para su propio ejemplo concreto, ni siquiera almacenaría el namedtuple en la clase; use la salida directamente (no almacene la clase generada en absoluto), o use una global:

 from collections import namedtuple State = namedtuple('State', ['name', 'capital']) class StateDatabase: db = [State(*args) for args in [ ('Alabama', 'Montgomery'), ('Alaska', 'Juneau'), # ... ]] 

En mi opinión, es un defecto en Python 3. Espero que lo cambien.

Old Way (funciona en 2.7, lanza NameError: name 'x' is not defined en 3+):

 class A: x = 4 y = [x+i for i in range(1)] 

NOTA: simplemente el scope con Ax no lo resolvería

Nueva forma (funciona en 3+):

 class A: x = 4 y = (lambda x=x: [x+i for i in range(1)])() 

Debido a que la syntax es tan fea, simplemente inicializo todas mis variables de clase en el constructor normalmente

La respuesta aceptada proporciona información excelente, pero parece haber algunas otras arrugas aquí: diferencias entre comprensión de lista y expresiones generadoras. Una demo con la que jugué:

 class Foo: # A class-level variable. X = 10 # I can use that variable to define another class-level variable. Y = sum((X, X)) # Works in Python 2, but not 3. # In Python 3, list comprehensions were given their own scope. try: Z1 = sum([X for _ in range(3)]) except NameError: Z1 = None # Fails in both. # Apparently, generator expressions (that's what the entire argument # to sum() is) did have their own scope even in Python 2. try: Z2 = sum(X for _ in range(3)) except NameError: Z2 = None # Workaround: put the computation in lambda or def. compute_z3 = lambda val: sum(val for _ in range(3)) # Then use that function. Z3 = compute_z3(X) # Also worth noting: here I can refer to XS in the for-part of the # generator expression (Z4 works), but I cannot refer to XS in the # inner-part of the generator expression (Z5 fails). XS = [15, 15, 15, 15] Z4 = sum(val for val in XS) try: Z5 = sum(XS[i] for i in range(len(XS))) except NameError: Z5 = None print(Foo.Z1, Foo.Z2, Foo.Z3, Foo.Z4, Foo.Z5) 

Este es un error en Python. Se anuncian las comprensiones como equivalentes a los bucles, pero esto no es cierto en las clases. Al menos hasta Python 3.6.6, en una comprensión utilizada en una clase, solo se puede acceder a una variable desde fuera de la comprensión dentro de la comprensión, y debe usarse como el iterador más externo. En una función, esta limitación de scope no se aplica.

Para ilustrar por qué esto es un error, volvamos al ejemplo original. Esto falla:

 class Foo: x = 5 y = [x for i in range(1)] 

Pero esto funciona:

 def Foo(): x = 5 y = [x for i in range(1)] 

La limitación se indica al final de esta sección en la guía de referencia.

Como el iterador más externo se evalúa en el ámbito circundante, podemos usar zip junto con itertools.repeat para llevar las dependencias al scope de la comprensión:

 import itertools as it class Foo: x = 5 y = [j for i, j in zip(range(3), it.repeat(x))] 

También se puede utilizar nesteds for bucles en la comprensión e incluir las dependencias en el iterable más externo:

 class Foo: x = 5 y = [j for j in (x,) for i in range(3)] 

Para el ejemplo específico del OP:

 from collections import namedtuple import itertools as it class StateDatabase: State = namedtuple('State', ['name', 'capital']) db = [State(*args) for State, args in zip(it.repeat(State), [ ['Alabama', 'Montgomery'], ['Alaska', 'Juneau'], # ... ])]