Uzyskiwanie dostępu do zmiennych klasowych z listy w definicji klasy

174

W jaki sposób uzyskujesz dostęp do innych zmiennych klasy z listy złożonej w definicji klasy? Poniższe działa w Pythonie 2, ale kończy się niepowodzeniem w Pythonie 3:

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

Python 3.2 podaje błąd:

NameError: global name 'x' is not defined

Próbowanie Foo.xteż nie działa. Jakieś pomysły, jak to zrobić w Pythonie 3?

Nieco bardziej skomplikowany motywujący przykład:

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

W tym przykładzie apply()byłoby to przyzwoite obejście, ale niestety zostało usunięte z Pythona 3.

Mark Lodato
źródło
Twój komunikat o błędzie jest nieprawidłowy. Dostaję się NameError: global name 'x' is not definedna Pythona 3.2 i 3.3, czego bym się spodziewał.
Martijn Pieters
Interesujące ... Jednym z oczywistych obejść jest przypisanie y po wyjściu z definicji klasy. Foo.y = [Foo.x for i in range (1)]
gps,
3
+ martijn-pieters link do duplikatu ma rację, jest tam komentarz od + matt-b z wyjaśnieniem: Listy składane w Pythonie 2.7 nie mają własnej przestrzeni nazw (w przeciwieństwie do wyrażeń typu set lub dict lub wyrażeń generatora ... zamień swoje [ ] z {}, aby zobaczyć to w akcji). Oni wszyscy mają swoją własną przestrzeń nazw w 3.
gps
@gps: Lub użyj zakresu zagnieżdżonego, wstawiając (tymczasową) funkcję do zestawu definicji klas.
Martijn Pieters
Właśnie przetestowałem 2.7.11. Mam błąd nazwy
Junchao Gu

Odpowiedzi:

244

Zakres klas i lista, zestaw lub słownik, a także wyrażenia generatora nie mieszają się.

Dlaczego; lub oficjalne słowo na ten temat

W Pythonie 3 wyrażeniom listowym nadano własny odpowiedni zakres (lokalną przestrzeń nazw), aby zapobiec przedostawaniu się ich zmiennych lokalnych do otaczającego zakresu (zobacz Python list comprinding nazwy nawet po zakresie zrozumienia. Czy to prawda? ). To świetnie, gdy używasz takiego rozumienia list w module lub w funkcji, ale w klasach zakres jest trochę, hm, dziwny .

Jest to udokumentowane w pep 227 :

Nazwy w zakresie klas nie są dostępne. Nazwy są rozwiązywane w najbardziej wewnętrznym obejmującym zakresie funkcji. Jeśli definicja klasy występuje w łańcuchu zagnieżdżonych zasięgów, proces rozstrzygania pomija definicje klas.

aw classdokumentacji złożonej :

Zestaw klasy jest następnie wykonywany w nowej ramce wykonawczej (zob. Sekcja Nazewnictwo i powiązanie ), przy użyciu nowo utworzonej lokalnej przestrzeni nazw i oryginalnej globalnej przestrzeni nazw. (Zwykle pakiet zawiera tylko definicje funkcji). Kiedy zestaw klasy kończy wykonywanie, ramka wykonania jest odrzucana, ale lokalna przestrzeń nazw jest zapisywana . [4] Obiekt klasy jest następnie tworzony przy użyciu listy dziedziczenia dla klas podstawowych i zapisanej lokalnej przestrzeni nazw dla słownika atrybutów.

Podkreśl moje; ramą wykonania jest tymczasowy zakres.

Ponieważ zakres jest zmieniany jako atrybuty obiektu klasy, zezwolenie na użycie go jako zakresu nielokalnego również prowadzi do niezdefiniowanego zachowania; co by się stało, gdyby metoda klasy określana xjako zagnieżdżona zmienna zasięgu Foo.x, na przykład również manipulowała ? Co ważniejsze, co to oznaczałoby dla podklas Foo? Python musi inaczej traktować zakres klasy, ponieważ bardzo różni się on od zakresu funkcji.

Wreszcie, połączona sekcja Nazewnictwa i powiązań w dokumentacji modelu wykonywania wymienia wyraźnie zakresy klas:

Zakres nazw zdefiniowanych w bloku klasy jest ograniczony do bloku klasy; nie rozciąga się na bloki kodu metod - obejmuje to wyrażenia i wyrażenia generatora, ponieważ są one implementowane przy użyciu zakresu funkcji. Oznacza to, że następujące działania zakończą się niepowodzeniem:

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

Podsumowując: nie można uzyskać dostępu do zakresu klasy z funkcji, wyrażeń listowych lub wyrażeń generatora zawartych w tym zakresie; działają tak, jakby ten zakres nie istniał. W Pythonie 2 listy składane zostały zaimplementowane za pomocą skrótu, ale w Pythonie 3 otrzymały swój własny zakres funkcji (tak, jak powinny były mieć przez cały czas) i dlatego Twój przykład się psuje. Inne typy rozumienia mają swój własny zakres niezależnie od wersji Pythona, więc podobny przykład ze zrozumieniem zestawu lub dyktu nie działa w Pythonie 2.

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

(Mały) wyjątek; lub dlaczego jedna część może nadal działać

Istnieje jedna część wyrażenia zrozumienia lub wyrażenia generatora, która jest wykonywana w otaczającym zakresie, niezależnie od wersji języka Python. To byłby wyraz najbardziej zewnętrznej iterowalności. W twoim przykładzie jest to range(1):

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

Zatem użycie xw tym wyrażeniu nie spowodowałoby błędu:

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

Dotyczy to tylko najbardziej zewnętrznych iterowalnych; jeśli zrozumienie ma wiele forklauzul, iterowalne dla forklauzul wewnętrznych są oceniane w zakresie zrozumienia:

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

Ta decyzja projektowa została podjęta, aby zgłosić błąd w czasie tworzenia genexp zamiast czasu iteracji, gdy tworzenie najbardziej zewnętrznej iterowalnej ekspresji generatora zgłasza błąd lub gdy najbardziej zewnętrzna iterowalna okazuje się nie być iterowalna. Zrozumienia dzielą to zachowanie dla spójności.

Patrząc pod maskę; lub o wiele więcej szczegółów niż kiedykolwiek chciałeś

Możesz zobaczyć to wszystko w akcji za pomocą dismodułu . W poniższych przykładach używam języka Python 3.3, ponieważ dodaje on nazwy kwalifikowane, które dokładnie identyfikują obiekty kodu, które chcemy sprawdzić. Wytworzony kod bajtowy jest poza tym funkcjonalnie identyczny z Pythonem 3.2.

Aby utworzyć klasę, Python w zasadzie bierze cały zestaw, który tworzy ciało klasy (więc wszystko jest wcięte o jeden poziom głębiej niż class <name>:linia) i wykonuje to tak, jakby to była funkcja:

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

Pierwsza z LOAD_CONSTnich ładuje obiekt kodu dla Footreści klasy, następnie przekształca go w funkcję i wywołuje. Wynikiem tego połączenia jest następnie wykorzystywany do tworzenia nazw klasy, jej __dict__. Na razie w porządku.

Należy tu zauważyć, że kod bajtowy zawiera zagnieżdżony obiekt kodu; w Pythonie definicje klas, funkcje, wyrażenia i generatory są reprezentowane jako obiekty kodu, które zawierają nie tylko kod bajtowy, ale także struktury reprezentujące zmienne lokalne, stałe, zmienne pobrane z globalnych i zmienne pobrane z zakresu zagnieżdżonego. Skompilowany kod bajtowy odnosi się do tych struktur, a interpreter Pythona wie, jak uzyskać dostęp do tych, które mają przedstawione kody bajtowe.

Ważną rzeczą do zapamiętania jest to, że Python tworzy te struktury w czasie kompilacji; classapartament jest obiektem kod ( <code object Foo at 0x10a436030, file "<stdin>", line 2>), która jest już skompilowany.

Przyjrzyjmy się temu obiektowi kodu, który tworzy samą treść klasy; obiekty kodu mają co_constsstrukturę:

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

Powyższy kod bajtowy tworzy treść klasy. Funkcja jest wykonywana, a wynikowa locals()przestrzeń nazw, zawierająca xi yjest używana do tworzenia klasy (z wyjątkiem tego, że nie działa, ponieważ xnie jest zdefiniowana jako globalna). Zauważ, że po zapisaniu 5w x, ładuje inny obiekt kodu; to jest rozumienie listy; jest opakowany w obiekt funkcji, tak jak było w treści klasy; utworzona funkcja przyjmuje argument pozycyjny, range(1)iterowalny do użycia w swoim kodzie pętli, rzutowany na iterator. Jak pokazano w kodzie bajtowym, range(1)jest oceniany w zakresie klasy.

Z tego widać, że jedyną różnicą między obiektem kodu dla funkcji lub generatora a obiektem kodu dla zrozumienia jest to, że ten ostatni jest wykonywany natychmiast po wykonaniu obiektu kodu nadrzędnego; kod bajtowy po prostu tworzy funkcję w locie i wykonuje ją w kilku małych krokach.

Python 2.x zamiast tego używa wbudowanego kodu bajtowego, tutaj jest wyjście z Pythona 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        

Żaden obiekt kodu nie jest ładowany, zamiast tego FOR_ITERuruchamiana jest pętla. Tak więc w Pythonie 3.x generator list otrzymał własny, właściwy obiekt kodu, co oznacza, że ​​ma swój własny zakres.

Jednak interpretacja została skompilowana wraz z resztą kodu źródłowego w języku Python, gdy moduł lub skrypt został po raz pierwszy załadowany przez interpreter, a kompilator nie uważa zestawu klas za prawidłowy zakres. Wszelkie zmienne, do których odwołuje się lista, muszą odnosić się rekurencyjnie do zakresu otaczającego definicję klasy. Jeśli zmienna nie została znaleziona przez kompilator, oznacza ją jako globalną. Demontaż obiektu kodu zrozumienia listy pokazuje, że xfaktycznie jest załadowany jako globalny:

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

Ten fragment kodu bajtowego ładuje pierwszy przekazany argument ( range(1)iterator) i podobnie jak wersja Python 2.x używa go FOR_ITERdo zapętlenia nad nim i utworzenia wyjścia.

Gdybyśmy zamiast tego zdefiniowali xw foofunkcji, xbyłaby to zmienna komórki (komórki odnoszą się do zagnieżdżonych zakresów):

>>> 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_DEREFPośrednio załadować xz obiektów komórkowych obiekt Kod:

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

Rzeczywiste odwołanie wyszukuje wartość z bieżących struktur danych ramki, które zostały zainicjowane z .__closure__atrybutu obiektu funkcji . Ponieważ funkcja utworzona dla obiektu kodu zrozumienia jest ponownie odrzucana, nie możemy sprawdzić zamknięcia tej funkcji. Aby zobaczyć zamknięcie w akcji, musielibyśmy zamiast tego sprawdzić zagnieżdżoną funkcję:

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

Podsumowując:

  • Listy składane otrzymują własne obiekty kodu w Pythonie 3 i nie ma różnicy między obiektami kodu dla funkcji, generatorów lub wyrażeń; obiekty kodu zrozumienia są opakowane w tymczasowy obiekt funkcji i natychmiast wywołane.
  • Obiekty kodu są tworzone w czasie kompilacji, a wszelkie zmienne nielokalne są oznaczane jako zmienne globalne lub wolne, na podstawie zagnieżdżonych zakresów kodu. Treść klasy nie jest uważana za zakres wyszukiwania tych zmiennych.
  • Podczas wykonywania kodu Python musi tylko zajrzeć do globalnych lub zamknięcia aktualnie wykonywanego obiektu. Ponieważ kompilator nie uwzględnił treści klasy jako zakresu, tymczasowa przestrzeń nazw funkcji nie jest uwzględniana.

Obejście; lub co z tym zrobić

Jeśli było stworzyć wyraźny zakres dla xzmiennej, podobnie jak w funkcji, to można używać zmiennych klasy zakres wyrażeń listowych:

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

Funkcję „tymczasową” ymożna wywołać bezpośrednio; zastępujemy go, gdy robimy z jego wartością zwracaną. Jego zakres jest brany pod uwagę przy rozwiązywaniu 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',)

Oczywiście ludzie czytający twój kod podrapią się trochę po głowie; możesz zamieścić tam duży gruby komentarz wyjaśniający, dlaczego to robisz.

Najlepszym obejściem jest użycie __init__do utworzenia zmiennej instancji:

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

i unikaj drapania się po głowie i pytań, aby się wytłumaczyć. Dla twojego konkretnego przykładu, nie zapisałbym nawet tego namedtuplena zajęciach; użyj wyjścia bezpośrednio (w ogóle nie przechowuj wygenerowanej klasy) lub użyj globalnego:

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

class StateDatabase:
    db = [State(*args) for args in [
       ('Alabama', 'Montgomery'),
       ('Alaska', 'Juneau'),
       # ...
    ]]
Martijn Pieters
źródło
21
Możesz także użyć lambdy, aby naprawić wiązanie:y = (lambda x=x: [x for i in range(1)])()
ecatmur
3
@ecatmur: Dokładnie, w końcu lambdato tylko anonimowe funkcje.
Martijn Pieters
2
Dla przypomnienia, obejście, które używa domyślnego argumentu (do lambdy lub funkcji) do przekazania zmiennej klasy, ma problem. Mianowicie przekazuje aktualną wartość zmiennej. Tak więc, jeśli zmienna zmieni się później, a następnie zostanie wywołana lambda lub funkcja, lambda lub funkcja będzie używać starej wartości. To zachowanie różni się od zachowania zamknięcia (które przechwytuje odniesienie do zmiennej, a nie jej wartość), więc może być nieoczekiwane.
Neal Young
9
Jeśli wymaga strony z informacjami technicznymi, aby wyjaśnić, dlaczego coś nie działa intuicyjnie, nazywam to błędem.
Jonathan
5
@JonathanLeaders: Nie nazywaj tego błędem , nazwij to kompromisem . Jeśli chcesz A i B, ale możesz dostać tylko jeden z nich, to bez względu na to, jak zdecydujesz, w niektórych sytuacjach nie spodoba ci się wynik. To jest życie.
Lutz Prechelt
15

Moim zdaniem jest to wada w Pythonie 3. Mam nadzieję, że to zmienią.

Old Way (działa w 2.7, rzuca NameError: name 'x' is not definedw 3+):

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

UWAGA: samo określenie zakresu A.xnie rozwiązałoby problemu

New Way (działa w 3+):

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

Ponieważ składnia jest tak brzydka, po prostu inicjalizuję wszystkie moje zmienne klasy w konstruktorze

Jonathan
źródło
6
Problem występuje również w Pythonie 2, gdy używamy wyrażeń generatora, a także wyrażeń ze zbioru i słownika. To nie jest błąd, jest to konsekwencja działania przestrzeni nazw klas. To się nie zmieni.
Martijn Pieters
4
I zauważam, że twoje obejście robi dokładnie to, co już stwierdza moja odpowiedź: utwórz nowy zakres (lambda nie różni się tutaj od użycia defdo utworzenia funkcji).
Martijn Pieters
1
tak. Chociaż miło jest mieć odpowiedź na pierwszy rzut oka, to ten błędnie określa zachowanie jako błąd, gdy jest to efekt uboczny sposobu działania języka (i dlatego nie zostanie zmieniony)
jsbueno
To jest inny problem, który w rzeczywistości nie jest problemem w Pythonie 3. Występuje tylko w IPythonie, gdy wywołujesz go w trybie osadzania za pomocą say python -c "import IPython;IPython.embed()". Uruchom IPython bezpośrednio za pomocą say, ipythona problem zniknie.
Riaz Rizvi
6

Przyjęta odpowiedź dostarcza doskonałych informacji, ale wydaje się, że jest tu kilka innych zmarszczek - różnice między zrozumieniem listy a wyrażeniami generatora. Demo, z którym się bawiłem:

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)
FMc
źródło
2

To jest błąd w Pythonie. Rozumienia są reklamowane jako równoważne pętlom for, ale nie jest to prawdą w klasach. Przynajmniej do Pythona 3.6.6, w zrozumieniu używanym w klasie, tylko jedna zmienna spoza zrozumienia jest dostępna wewnątrz zrozumienia i musi być używana jako najbardziej zewnętrzny iterator. W funkcji to ograniczenie zakresu nie ma zastosowania.

Aby zilustrować, dlaczego jest to błąd, wróćmy do oryginalnego przykładu. To się nie udaje:

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

Ale to działa:

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

Ograniczenie podano na końcu tej sekcji w przewodniku informacyjnym.

bzip2
źródło
1

Ponieważ najbardziej zewnętrzny iterator jest oceniany w otaczającym zakresie, możemy użyć ziprazem z, itertools.repeataby przenieść zależności do zakresu zrozumienia:

import itertools as it

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

Można również użyć zagnieżdżonych forpętli w zrozumieniu i uwzględnić zależności w najbardziej zewnętrznej iterowalnej:

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

Konkretny przykład PO:

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'],
        # ...
    ])]
gość
źródło