Accesso alle variabili di classe dalla comprensione di un elenco nella definizione della classe


174

Come si accede ad altre variabili di classe dalla comprensione di un elenco all'interno della definizione di classe? Quanto segue funziona in Python 2 ma non riesce in Python 3:

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

Python 3.2 dà l'errore:

NameError: global name 'x' is not defined

Anche provare Foo.xnon funziona. Qualche idea su come farlo in Python 3?

Un esempio motivante leggermente più complicato:

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

In questo esempio, apply()sarebbe stata una soluzione decente, ma è purtroppo rimosso da Python 3.


Il tuo messaggio di errore non è corretto Salgo NameError: global name 'x' is not definedsu Python 3.2 e 3.3 che è quello che mi aspetterei.
Martijn Pieters

Interessante ... Una soluzione ovvia è quella di assegnare y dopo aver chiuso la definizione di classe. Foo.y = [Foo.x per i nell'intervallo (1)]
gps

3
Il link + martijn-pieters a un duplicato è giusto, c'è un commento da + matt-b con la spiegazione: le comprensioni dell'elenco di Python 2.7 non hanno il loro spazio dei nomi (a differenza delle comprensioni stabilite o dettate o delle espressioni del generatore ... sostituisci il tuo [ ] con {} per vederlo in azione). Hanno tutti hanno il proprio spazio dei nomi in 3.
gps

@gps: oppure usa un ambito nidificato, inserendo una funzione (temporanea) nella suite di definizioni di classe.
Martijn Pieters

Ho appena provato il 2.7.11. Errore di nome
Junchao Gu

Risposte:


244

L'ambito della classe e la comprensione dell'elenco, dell'insieme o del dizionario, nonché le espressioni del generatore non si mescolano.

Il perché; o, la parola ufficiale su questo

In Python 3, alla comprensione dell'elenco è stato assegnato un ambito proprio (spazio dei nomi locale) proprio, per impedire che le loro variabili locali sanguinassero all'interno dell'ambito circostante (vedi la comprensione dell'elenco Python che collega i nomi anche dopo l'ambito della comprensione. È giusto? ). È fantastico quando si utilizza una tale comprensione di elenco in un modulo o in una funzione, ma nelle classi lo scoping è un po ' strano .

Questo è documentato nel pep 227 :

I nomi nell'ambito della classe non sono accessibili. I nomi vengono risolti nell'ambito della funzione di chiusura più interna. Se una definizione di classe si verifica in una catena di ambiti nidificati, il processo di risoluzione salta le definizioni di classe.

e nella classdocumentazione relativa all'istruzione composta :

La suite della classe viene quindi eseguita in un nuovo frame di esecuzione (vedere la sezione Denominazione e associazione ), utilizzando uno spazio dei nomi locale appena creato e lo spazio dei nomi globale originale. (Di solito, la suite contiene solo definizioni di funzioni.) Quando la suite della classe termina l'esecuzione, il suo frame di esecuzione viene scartato ma il suo spazio dei nomi locale viene salvato . [4] Un oggetto di classe viene quindi creato utilizzando l'elenco di ereditarietà per le classi di base e lo spazio dei nomi locale salvato per il dizionario degli attributi.

Enfasi mia; il frame di esecuzione è l'ambito temporaneo.

Poiché l'ambito viene riproposto come gli attributi di un oggetto di classe, consentendone l'utilizzo come ambito non locale porta anche a un comportamento indefinito; cosa accadrebbe se un metodo di classe indicato xcome una variabile di ambito nidificata, quindi manipolasse Foo.xanche, per esempio? Ancora più importante, cosa significherebbe per le sottoclassi di Foo? Python deve trattare un ambito di classe in modo diverso poiché è molto diverso da un ambito di funzione.

Infine, ma sicuramente non meno importante, la sezione Denominazione e associazione collegati nella documentazione del modello di esecuzione menziona esplicitamente gli ambiti di classe:

L'ambito dei nomi definiti in un blocco di classe è limitato al blocco di classe; non si estende ai blocchi di codice dei metodi - questo include comprensioni ed espressioni del generatore poiché sono implementate usando un ambito di funzione. Ciò significa che non riusciranno:

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

Quindi, per riassumere: non è possibile accedere all'ambito della classe da funzioni, comprensioni di elenchi o espressioni di generatori racchiuse in tale ambito; agiscono come se tale ambito non esistesse. In Python 2, le comprensioni degli elenchi sono state implementate usando una scorciatoia, ma in Python 3 hanno il loro ambito di funzioni (come avrebbero dovuto avere da sempre) e quindi il tuo esempio si interrompe. Altri tipi di comprensione hanno il loro ambito indipendentemente dalla versione di Python, quindi un esempio simile con una comprensione set o dict si spezzerebbe in Python 2.

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

La (piccola) eccezione; o, perché una parte potrebbe ancora funzionare

C'è una parte di un'espressione di comprensione o generatore che viene eseguita nell'ambito circostante, indipendentemente dalla versione di Python. Sarebbe l'espressione per l'iterabile più esterno. Nel tuo esempio, è il range(1):

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

Pertanto, l'utilizzo xin quell'espressione non genererebbe un errore:

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

Questo vale solo per l'iterabile più esterno; se una comprensione ha più forclausole, gli iterabili per le forclausole interne vengono valutati nell'ambito della comprensione:

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

Questa decisione di progettazione è stata presa al fine di generare un errore al momento della creazione di genexp invece del tempo di iterazione quando la creazione dell'iterabile più esterno di un'espressione del generatore genera un errore o quando l'iterabile più esterno risulta non essere iterabile. Le comprensioni condividono questo comportamento per coerenza.

Guardando sotto il cofano; o, molto più dettagli di quanto tu abbia mai desiderato

Puoi vedere tutto in azione usando il dismodulo . Sto usando Python 3.3 nei seguenti esempi, perché aggiunge nomi qualificati che identificano ordinatamente gli oggetti di codice che vogliamo ispezionare. Il bytecode prodotto è altrimenti identico dal punto di vista funzionale a Python 3.2.

Per creare una classe, Python essenzialmente prende l'intera suite che compone il corpo della classe (quindi tutto rientrava di un livello più in profondità della class <name>:linea), e lo esegue come se fosse una funzione:

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

Il primo LOAD_CONSTcarica un oggetto codice per il Foocorpo della classe, quindi lo trasforma in una funzione e lo chiama. Il risultato di quella chiamata viene quindi utilizzato per creare lo spazio dei nomi della classe, il suo __dict__. Fin qui tutto bene.

La cosa da notare qui è che il bytecode contiene un oggetto codice nidificato; in Python, le definizioni di classe, le funzioni, le comprensioni e i generatori sono tutti rappresentati come oggetti di codice che contengono non solo il bytecode, ma anche strutture che rappresentano variabili locali, costanti, variabili prese dai globi e variabili prese dall'ambito nidificato. Il bytecode compilato si riferisce a quelle strutture e l'interprete python sa come accedere a quelle date i bytecode presentati.

La cosa importante da ricordare qui è che Python crea queste strutture in fase di compilazione; la classsuite è un oggetto codice ( <code object Foo at 0x10a436030, file "<stdin>", line 2>) che è già stato compilato.

Ispezioniamo quell'oggetto di codice che crea il corpo della classe stesso; gli oggetti di codice hanno una co_constsstruttura:

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

Il bytecode sopra crea il corpo della classe. La funzione viene eseguita e lo locals()spazio dei nomi risultante , contenente xe yviene utilizzato per creare la classe (tranne per il fatto che non funziona perché xnon è definito come globale). Si noti che dopo la memorizzazione 5in x, si carica un altro codice oggetto; questa è la comprensione della lista; è racchiuso in un oggetto funzione proprio come il corpo della classe; la funzione creata prende un argomento posizionale, l' range(1)iterabile da usare per il suo codice di ciclo, trasmesso a un iteratore. Come mostrato nel bytecode, range(1)viene valutato nell'ambito della classe.

Da ciò si può vedere che l'unica differenza tra un oggetto di codice per una funzione o un generatore e un oggetto di codice per una comprensione è che quest'ultimo viene eseguito immediatamente quando viene eseguito l'oggetto di codice principale; il bytecode crea semplicemente una funzione al volo e la esegue in pochi piccoli passaggi.

Python 2.x usa invece il bytecode in linea lì, qui è l'output di 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        

Nessun oggetto di codice viene caricato, invece un FOR_ITERciclo viene eseguito in linea. Quindi in Python 3.x, al generatore di elenchi è stato assegnato un oggetto codice proprio, il che significa che ha un suo ambito.

Tuttavia, la comprensione è stata compilata insieme al resto del codice sorgente di Python quando il modulo o lo script sono stati caricati per la prima volta dall'interprete e il compilatore non considera una suite di classi un ambito valido. Qualsiasi variabile referenziata nella comprensione di un elenco deve guardare in modo ricorsivo nell'ambito che circonda la definizione della classe. Se la variabile non è stata trovata dal compilatore, la contrassegna come globale. Il disassemblaggio dell'oggetto codice di comprensione elenco mostra che xè effettivamente caricato come globale:

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

Questo blocco di bytecode carica il primo argomento passato (l' range(1)iteratore), e proprio come la versione Python 2.x usa FOR_ITERper eseguire il loop su di esso e creare il suo output.

Se avessimo definito xnella foofunzione invece, xsarebbe stata una variabile di cella (le celle si riferiscono a ambiti nidificati):

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

Il LOAD_DEREFsarà indirettamente caricare xdagli oggetti cellulari codice oggetto:

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

Il riferimento effettivo cerca il valore dalle strutture di dati del frame corrente, che sono state inizializzate dall'attributo di un oggetto funzione .__closure__. Poiché la funzione creata per l'oggetto codice di comprensione viene nuovamente scartata, non possiamo controllare la chiusura di quella funzione. Per vedere una chiusura in azione, dovremmo invece ispezionare una funzione nidificata:

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

Quindi, per riassumere:

  • Le comprensioni di elenchi ottengono i propri oggetti di codice in Python 3 e non vi è alcuna differenza tra oggetti di codice per funzioni, generatori o comprensioni; gli oggetti del codice di comprensione sono racchiusi in un oggetto funzione temporaneo e chiamati immediatamente.
  • Gli oggetti codice vengono creati in fase di compilazione e tutte le variabili non locali vengono contrassegnate come variabili globali o libere, in base agli ambiti nidificati del codice. Il corpo della classe non è considerato un ambito per la ricerca di tali variabili.
  • Quando si esegue il codice, Python deve solo guardare nei globi o la chiusura dell'oggetto attualmente in esecuzione. Poiché il compilatore non includeva il corpo della classe come ambito, lo spazio dei nomi delle funzioni temporanee non viene considerato.

Una soluzione alternativa; o, cosa fare al riguardo

Se si dovesse creare un ambito esplicito per la xvariabile, come in una funzione, è possibile utilizzare variabili con ambito di classe per una comprensione dell'elenco:

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

La funzione 'temporanea' ypuò essere chiamata direttamente; lo sostituiamo quando lo facciamo con il suo valore di ritorno. Il suo scopo viene preso in considerazione quando si risolve 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',)

Ovviamente, le persone che leggono il tuo codice si grattano un po 'la testa; potresti voler inserire un grande commento grasso che spiega perché lo stai facendo.

La soluzione migliore è usare semplicemente __init__per creare una variabile di istanza invece:

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

ed evita tutti i graffi alla testa e le domande per spiegarti. Per il tuo esempio concreto, non memorizzerei nemmeno namedtuplela lezione; utilizzare direttamente l'output (non memorizzare affatto la classe generata) o utilizzare un valore globale:

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

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

21
Puoi anche usare un lambda per correggere l'associazione:y = (lambda x=x: [x for i in range(1)])()
ecatmur

3
@ecatmur: Esatto lambda, dopo tutto sono solo funzioni anonime.
Martijn Pieters

2
Per la cronaca, la soluzione che utilizza un argomento predefinito (a una lambda o una funzione) per passare nella variabile di classe ha un gotcha. Vale a dire, passa il valore corrente della variabile. Quindi, se la variabile cambia in seguito, e quindi viene chiamata lambda o funzione, lambda o funzione utilizzerà il vecchio valore. Questo comportamento differisce dal comportamento di una chiusura (che catturerebbe un riferimento alla variabile, piuttosto che il suo valore), quindi potrebbe essere inaspettato.
Neal Young,

9
Se richiede una pagina di informazioni tecniche per spiegare perché qualcosa non funziona in modo intuitivo, lo chiamo bug.
Jonathan,

5
@JonathanLeaders: non chiamarlo un bug , chiamalo un compromesso . Se vuoi A e B, ma puoi ottenerne solo uno, non importa come decidi, in alcune situazioni non ti piacerà il risultato. È la vita.
Lutz Prechelt,

15

Secondo me è un difetto in Python 3. Spero che lo cambino.

Old Way (funziona in 2.7, lancia NameError: name 'x' is not definedin 3+):

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

NOTA: il semplice scoping con A.xnon lo risolverebbe

New Way (funziona in 3+):

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

Poiché la sintassi è così brutta, in genere ho appena inizializzato tutte le mie variabili di classe nel costruttore


6
Il problema è presente anche in Python 2, quando si usano le espressioni del generatore, così come con la comprensione di set e dizionari. Non è un bug, è una conseguenza di come funzionano gli spazi dei nomi delle classi. Non cambierà.
Martijn Pieters

4
E noto che la tua soluzione alternativa fa esattamente quello che la mia risposta afferma già: creare un nuovo ambito (qui una lambda non è diversa dall'uso defper creare una funzione).
Martijn Pieters

1
Sì. Mentre è bello avere una risposta con il work-around a colpo d'occhio, questo indica incorrecamente il comportamento come un bug, quando è un effetto collaterale del modo in cui funziona la lingua (e quindi non verrà modificato)
jsbueno,

Questo è un problema diverso, che in realtà non è un problema in Python 3. Si verifica solo in IPython quando lo chiami in modalità embed usando say python -c "import IPython;IPython.embed()". Esegui IPython direttamente usando say ipythone il problema scomparirà.
Riaz Rizvi,

6

La risposta accettata fornisce informazioni eccellenti, ma qui sembrano esserci alcune altre rughe: differenze tra la comprensione dell'elenco e le espressioni del generatore. Una demo con cui ho giocato:

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

Questo è un bug in Python. Le comprensioni sono pubblicizzate come equivalenti a per i loop, ma questo non è vero nelle classi. Almeno fino a Python 3.6.6, in una comprensione usata in una classe, solo una variabile dall'esterno della comprensione è accessibile all'interno della comprensione e deve essere usata come iteratore più esterno. In una funzione, questa limitazione dell'ambito non si applica.

Per illustrare perché questo è un bug, torniamo all'esempio originale. Questo fallisce:

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

Ma questo funziona:

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

La limitazione è indicata alla fine di questa sezione nella guida di riferimento.


1

Poiché l'iteratore più esterno viene valutato nell'ambito circostante, possiamo usare zipinsieme itertools.repeatper portare le dipendenze al campo di comprensione:

import itertools as it

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

Si può anche usare forloop nidificati nella comprensione e includere le dipendenze nell'iterabile più esterno:

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

Per l'esempio specifico dell'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'],
        # ...
    ])]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.