Acceso a variables de clase desde una comprensión de la lista en la definición de clase


174

¿Cómo se accede a otras variables de clase desde una comprensión de lista 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.xtampoco funciona. ¿Alguna idea sobre cómo hacer esto en Python 3?

Un ejemplo motivador 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()habría sido una solución decente, pero lamentablemente se eliminó de Python 3.


Su mensaje de error es incorrecto. Me meto NameError: global name 'x' is not defineden Python 3.2 y 3.3, que es lo que esperaría.
Martijn Pieters

Interesante ... Una solución obvia es asignar y después de salir de la definición de clase. Foo.y = [Foo.x para i en el rango (1)]
gps

3
El enlace + martijn-pieters a un duplicado es correcto, hay un comentario de + matt-b allí con la explicación: las comprensiones de la lista Python 2.7 no tienen su propio espacio de nombres (a diferencia de las comprensiones establecidas o dictadas o las expresiones generadoras ... reemplace su [ ] con {} para ver eso en acción). Ellos todos tienen su propio espacio de nombres en 3.
gps

@gps: O use un ámbito anidado, insertando una función (temporal) en la suite de definición de clase.
Martijn Pieters

Acabo de probar en 2.7.11. Error de nombre
Junchao Gu

Respuestas:


244

El alcance de la clase y las comprensiones de listas, conjuntos o diccionarios, así como las expresiones generadoras no se mezclan.

El porque; o la palabra oficial sobre esto

En Python 3, las comprensiones de listas recibieron un alcance propio (espacio de nombres local) propio, para evitar que sus variables locales se desborden en el alcance circundante (ver Comprensión de listas de Python volver a unir nombres incluso después del alcance de la comprensión. ¿Es esto correcto? ). Eso es genial cuando se usa dicha comprensión de lista en un módulo o en una función, pero en las clases, el alcance es un poco, uhm, extraño .

Esto está documentado en pep 227 :

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

y en la classdocumentación de la declaración compuesta :

La suite de la clase se ejecuta 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, el conjunto contiene solo definiciones de funciones). Cuando el conjunto de la clase finaliza la ejecución, su marco de ejecución se descarta pero se guarda su espacio de nombres local . [4] Luego 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.

El énfasis es mío; El marco de ejecución es el alcance temporal.

Debido a que el alcance se reutiliza como los atributos en un objeto de clase, permitir que se use como un alcance no local también conduce a un comportamiento indefinido; ¿Qué pasaría si un método de clase denominado xvariable de ámbito anidado, luego manipula Foo.xtambién, por ejemplo? Más importante aún, ¿qué significaría eso para las subclases de Foo? Python tiene que tratar un alcance de clase de manera diferente ya que es muy diferente del alcance de una función.

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

El alcance 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 métodos, esto incluye comprensiones y expresiones generadoras, ya que se implementan utilizando un alcance 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 ámbito de clase desde funciones, listas de comprensiones o expresiones generadoras incluidas en ese ámbito; actúan como si ese alcance no existiera. En Python 2, las comprensiones de listas se implementaron usando un acceso directo, pero en Python 3 obtuvieron su propio alcance 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 alcance, independientemente de la versión de Python, por lo que un ejemplo similar con una comprensión establecida o dictada 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 aún puede funcionar

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 del iterable más externo. En su ejemplo, es el range(1):

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

Por lo tanto, usar xesa expresión no arrojaría un error:

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

Esto solo se aplica al iterable más externo; Si una comprensión tiene múltiples forcláusulas, los iterables para las forcláusulas internas se evalúan en el alcance 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 arrojar un error en el momento de la creación de genexp en lugar del tiempo de iteración cuando se crea el iterable más externo de una expresión generadora, o cuando el iterable más externo resulta no ser iterable. Las comprensiones comparten este comportamiento para mantener la coherencia.

Mirando debajo del capó; o, mucho más detalle del que siempre quisiste

Puedes ver todo esto en acción usando el dismódulo . 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 código de bytes 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 <name>: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 (<code object Foo at 0x10a436030, file "<stdin>", 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 primero LOAD_CONSTcarga un objeto de código para el Foocuerpo de la clase, luego lo convierte en una función y lo llama. El resultado de esa llamada se usa para crear el espacio de nombres de la clase, its __dict__. Hasta aquí todo bien.

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

Lo importante a recordar aquí es que Python crea estas estructuras en tiempo de compilación; la classsuite es un objeto de código ( <code object Foo at 0x10a436030, file "<stdin>", line 2>) que ya está compilado.

Inspeccionemos ese objeto de código que crea el cuerpo de la clase en sí; Los objetos de código tienen una co_constsestructura:

>>> foo.__code__.co_consts
(None, <code object Foo at 0x10a436030, file "<stdin>", 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.<locals>.Foo') 
             13 STORE_NAME               2 (__qualname__) 

  3          16 LOAD_CONST               1 (5) 
             19 STORE_NAME               3 (x) 

  4          22 LOAD_CONST               2 (<code object <listcomp> at 0x10a385420, file "<stdin>", line 4>) 
             25 LOAD_CONST               3 ('foo.<locals>.Foo.<listcomp>') 
             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 bytecode anterior crea el cuerpo de la clase. La función se ejecuta y el locals()espacio de nombres resultante , que contiene xy yse usa para crear la clase (excepto que no funciona porque xno está definido como global). Tenga en cuenta que después de almacenar 5en x, se 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)iterable para usar para su código de bucle, se convierte en un iterador. Como se muestra en el código de bytes, range(1)se evalúa en el alcance de la clase.

De esto 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 padre; el bytecode simplemente crea una función sobre la marcha y la ejecuta en unos pocos pasos pequeños.

Python 2.x usa código de bytes en línea allí, aquí se emite desde 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 FOR_ITERse ejecuta un bucle en línea. Entonces, en Python 3.x, el generador de listas recibió un objeto de código propio, lo que significa que tiene su propio alcance.

Sin embargo, la comprensión se compiló junto con el resto del código fuente de Python cuando el intérprete cargó por primera vez el módulo o el script, y el compilador no considera que un conjunto de clases sea un alcance válido. Cualquier variable referenciada en una lista de comprensión debe mirar en el ámbito que rodea la definición de clase, de forma recursiva. Si el compilador no encontró la variable, la marca como global. El desmontaje del objeto de código de comprensión de la lista muestra que, de xhecho, se carga como global:

>>> foo.__code__.co_consts[1].co_consts
('foo.<locals>.Foo', 5, <code object <listcomp> at 0x10a385420, file "<stdin>", line 4>, 'foo.<locals>.Foo.<listcomp>', 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 porción de código de bytes carga el primer argumento pasado (el range(1)iterador), y al igual que la versión Python 2.x usa FOR_ITERpara recorrerlo y crear su salida.

Si hubiéramos definido xen la foofunción, xsería una variable de celda (las celdas se refieren a ámbitos anidados):

>>> 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         

La LOAD_DEREFcarga indirecta xde 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 desde las estructuras de datos de trama actuales, que se inicializaron desde el .__closure__atributo de un objeto de función . Dado que la función creada para el objeto de código de comprensión se descarta nuevamente, no podemos 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 listas 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 de inmediato.
  • Los objetos de código se crean en tiempo de compilación y las variables no locales se marcan como variables globales o libres, según los ámbitos anidados 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 mirar a los globales, o al cierre del objeto que se está ejecutando actualmente. Como el compilador no incluyó el cuerpo de la clase como ámbito, no se considera el espacio de nombres de la función temporal.

Una solución alternativa; o qué hacer al respecto

Si fuera a crear un alcance explícito para la xvariable, como en una función, puede usar variables de alcance de clase para una comprensión de la lista:

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

La función 'temporal' yse puede llamar directamente; lo reemplazamos cuando lo hacemos con su valor de retorno. Su alcance se considera al resolver x:

>>> foo.__code__.co_consts[1].co_consts[2]
<code object y at 0x10a5df5d0, file "<stdin>", line 4>
>>> foo.__code__.co_consts[1].co_consts[2].co_cellvars
('x',)

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

La mejor solución 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 evite rascarse la cabeza y preguntas para explicarse. Para su propio ejemplo concreto, ni siquiera lo guardaría namedtupleen la clase; use la salida directamente (no almacene la clase generada en absoluto) o use un global:

from collections import namedtuple
State = namedtuple('State', ['name', 'capital'])

class StateDatabase:
    db = [State(*args) for args in [
       ('Alabama', 'Montgomery'),
       ('Alaska', 'Juneau'),
       # ...
    ]]

21
También puede usar una lambda para arreglar el enlace:y = (lambda x=x: [x for i in range(1)])()
ecatmur

3
@ecatmur: Exactamente, lambdason solo funciones anónimas, después de todo.
Martijn Pieters

2
Para el registro, la solución que usa un argumento predeterminado (para una lambda o una función) para pasar la variable de clase tiene un gotcha. A saber, pasa el valor actual de la variable. Entonces, si la variable cambia más tarde, y luego se llama a la función o lambda, la función o lambda utilizará el valor anterior. Este comportamiento difiere del comportamiento de un cierre (que capturaría una referencia a la variable, en lugar de su valor), por lo que puede ser inesperado.
Neal Young

9
Si requiere una página de información técnica para explicar por qué algo no funciona intuitivamente, lo llamo un error.
Jonathan

55
@ JonathanLeaders: No lo llames un error , llámalo una compensación . Si desea A y B, pero puede obtener solo uno de ellos, no importa cómo lo decida, en algunas situaciones no le gustará el resultado. Así es la vida.
Lutz Prechelt el

15

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

Old Way (funciona en 2.7, incluye NameError: name 'x' is not defined3+):

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

NOTA: simplemente determinarlo A.xno 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 sintaxis es tan fea, simplemente inicializo todas mis variables de clase en el constructor típicamente


66
El problema también está presente en Python 2, cuando se usan expresiones generadoras, así como con las comprensiones de conjuntos y diccionarios. No es un error, es una consecuencia de cómo funcionan los espacios de nombres de clase. No va a cambiar
Martijn Pieters

44
Y noto que su solución hace exactamente lo que mi respuesta ya dice: crear un nuevo alcance (una lambda no es diferente aquí de usar defpara crear una función).
Martijn Pieters

1
Sí. Si bien es bueno tener una respuesta con la solución de un vistazo, esta afirma incorrectamente el comportamiento como un error, cuando es un efecto secundario de la forma en que funciona el lenguaje (y, por lo tanto, no se cambiará)
jsbueno

Este es un problema diferente, que en realidad no es un problema en Python 3. Solo ocurre en IPython cuando lo llamas en modo de inserción usando say python -c "import IPython;IPython.embed()". Ejecute IPython directamente usando say ipythony el problema desaparecerá.
Riaz Rizvi

6

La respuesta aceptada proporciona información excelente, pero parece haber algunas otras arrugas aquí: diferencias entre la comprensión de la lista y las 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)

2

Este es un error en Python. Las comprensiones se anuncian como equivalentes para bucles for, pero esto no es cierto en las clases. Al menos hasta Python 3.6.6, en una comprensión utilizada en una clase, solo una variable desde fuera de la comprensión es accesible dentro de la comprensión, y debe usarse como el iterador más externo. En una función, esta limitación de alcance 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.


1

Dado que el iterador más externo se evalúa en el alcance circundante, podemos usarlo zipjunto con itertools.repeatpara llevar las dependencias al alcance 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 pueden usar forbucles anidados en la comprensión e incluir las dependencias en el iterativo 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'],
        # ...
    ])]
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.