Sınıf tanımındaki liste kavramalarından sınıf değişkenlerine erişme


174

Sınıf tanımındaki liste kavrayışından diğer sınıf değişkenlerine nasıl erişirsiniz? Python 2'de aşağıdakiler çalışır, ancak Python 3'te başarısız olur:

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

Python 3.2 şu hatayı verir:

NameError: global name 'x' is not defined

Denemek Foo.xde işe yaramıyor. Python 3'te bunun nasıl yapılacağı hakkında bir fikriniz var mı?

Biraz daha karmaşık motive edici bir örnek:

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

Bu örnekte, apply()iyi bir geçici çözüm olurdu, ancak Python 3'ten ne yazık ki kaldırıldı.


Hata mesajınız yanlış. Ben olsun NameError: global name 'x' is not definedPython 3.2 ve ben ne beklenir 3.3.
Martijn Pieters

İlginç ... Açık bir çözüm, sınıf tanımından çıktıktan sonra y atamaktır. Foo.y = [(1) aralığında i için Foo.x]
gps

3
+ martijn-pieters bir kopyasına bağlantı doğrudur, açıklamada + matt-b'den bir yorum var: Python 2.7 liste kavrayışlarının kendi ad alanları yok (set veya dikte anlama veya jeneratör ifadelerinin aksine ... [ ] ile eylemde görmek üzere). Onlar her 3'te kendi ad var
gps

@gps: Veya sınıf tanımlama paketine (geçici) bir işlev ekleyerek iç içe geçmiş bir kapsam kullanın.
Martijn Pieters

Az önce 2.7.11'de test ettim. İsim hatası var
Junchao Gu

Yanıtlar:


244

Sınıf kapsamı ve listesi, küme veya sözlük anlamlarının yanı sıra jeneratör ifadeleri karışmaz.

Neden; veya bu konudaki resmi kelime

Python 3'te, liste değişkenlerine, yerel değişkenlerinin çevreleyen kapsama geçmesini önlemek için kendilerine ait uygun bir kapsam (yerel ad alanı) verilmiştir (bkz. Python listesi anlama, anlama kapsamından sonra bile isimleri yeniden hatırlatır . Bir modülde veya işlevde böyle bir liste kavrayışı kullanırken bu harika, ancak sınıflarda kapsam belirleme biraz, uhm, garip .

Bu, pep 227'de belgelenmiştir :

Sınıf kapsamındaki adlara erişilemez. İsimler, en içteki kapatma işlevi kapsamında çözümlenir. İç içe kapsamlar zincirinde bir sınıf tanımı oluşursa, çözümleme işlemi sınıf tanımlarını atlar.

ve classbileşik beyan belgelerinde :

Daha sonra sınıfın paketi, yeni oluşturulan bir yerel ad alanı ve orijinal genel ad alanı kullanılarak yeni bir yürütme çerçevesinde yürütülür (bkz. Bölüm Adlandırma ve ciltleme ). (Genellikle, paket yalnızca işlev tanımlarını içerir.) Sınıfın paketi yürütmeyi bitirdiğinde , yürütme çerçevesi atılır ancak yerel ad alanı kaydedilir . [4] Daha sonra temel sınıflar için devralma listesi ve öznitelik sözlüğü için kaydedilmiş yerel ad alanı kullanılarak bir sınıf nesnesi oluşturulur.

Vurgu madeni; yürütme çerçevesi geçici kapsamdır.

Kapsam, bir sınıf nesnesindeki öznitelikler olarak yeniden konumlandırıldığından, yerel olmayan bir kapsam olarak kullanılmasına izin vermek, tanımlanmamış davranışa yol açar; xiç içe kapsam değişkeni olarak adlandırılan bir sınıf yöntemi Foo.x, örneğin de manipüle ederse ne olur ? Daha da önemlisi, bu alt sınıflar için ne anlama gelir Foo? Python , bir sınıf kapsamını işlev kapsamından çok farklı olduğu için farklı şekilde ele almalıdır.

Son olarak, ama en önemlisi, Yürütme modeli belgelerindeki bağlantılı Adlandırma ve bağlama bölümünde sınıf kapsamlarından açıkça bahsedilmektedir:

Sınıf bloğunda tanımlanan adların kapsamı sınıf bloğuyla sınırlıdır; yöntemlerin kod bloklarını kapsamaz - bu, bir işlev kapsamı kullanılarak uygulandıkları için kavrayışları ve üretici ifadelerini içerir. Bu, aşağıdakilerin başarısız olacağı anlamına gelir:

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

Özetlemek gerekirse, sınıf kapsamına o kapsamdaki fonksiyonlar, liste kavrayışları veya üretici ifadelerinden erişemezsiniz; bu kapsam yokmuş gibi davranırlar. Python 2'de liste kavrayışları bir kısayol kullanılarak uygulandı, ancak Python 3'te kendi işlev kapsamına sahiplerdi (baştan beri olması gerektiği gibi) ve böylece örnek kopuyorlar. Diğer anlama türlerinin Python sürümüne bakılmaksızın kendi kapsamları vardır, bu nedenle Python 2'de küme veya dikte anlama ile benzer bir örnek kırılacaktır.

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

(Küçük) istisna; ya, neden bir kısım may eser hala

Python sürümüne bakılmaksızın, çevreleyen kapsamda yürütülen bir anlama veya oluşturma ifadesinin bir kısmı vardır. Bu, en dıştaki yinelenebilir olanın ifadesi olurdu. Örneğinizde range(1):

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

Bu nedenle, xbu ifadede kullanmak bir hata atmaz:

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

Bu sadece en dıştaki yinelenebilirler için geçerlidir; bir kavrayışın birden fazla forcümlesi varsa, iç formaddeler için yinelenebilir değerler kavrama kapsamında değerlendirilir:

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

Bu tasarım kararı, bir jeneratör ifadesinin en dıştaki yinelemesini oluştururken bir hata atıldığında veya en dıştaki yinelemenin yinelenemez olmadığı ortaya çıktığında yineleme zamanı yerine genexp oluşturma zamanında bir hata atmak için verildi. Anlamalar bu davranışı tutarlılık için paylaşır.

Kaputun altına bakmak; ya da hiç istediğinden çok daha fazla ayrıntı

disModülü kullanarak bunların tümünü çalışırken görebilirsiniz . Aşağıdaki örneklerde Python 3.3 kullanıyorum , düzgün bir şekilde incelemek istediğimiz kod nesnelerini tanımlayan nitelikli adlar ekliyor . Üretilen bayt kodu aksi halde Python 3.2 ile işlevsel olarak aynıdır.

To oluşturmak bir sınıf, Python esasen (herşey bir seviye aşağıya daha girintili yüzden sınıf gövdesine oluşturan bütün paketi alır class <name>:çizgi) ve sanki bir işlev olduğunu yürüttüğü:

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

İlk önce sınıf gövdesi LOAD_CONSTiçin bir kod nesnesi yükler Foo, daha sonra bunu bir işleve dönüştürür ve çağırır. Bu çağrının sonucu daha sonra sınıfın ad alanını oluşturmak için kullanılır __dict__. Çok uzak çok iyi.

Burada dikkat edilmesi gereken nokta bayt kodunun iç içe bir kod nesnesi içermesidir; Python'da sınıf tanımları, fonksiyonlar, kavrayışlar ve üreteçlerin hepsi sadece bayt kodunu değil aynı zamanda yerel değişkenleri, sabitleri, globallerden alınan değişkenleri ve iç içe kapsamdan alınan değişkenleri temsil eden yapılar olarak kod nesneleri olarak temsil edilir. Derlenmiş bayt kodu bu yapıları ifade eder ve python yorumlayıcısı sunulan bayt kodlarına nasıl erişileceğini bilir.

Burada hatırlanması gereken önemli şey, Python'un bu yapıları derleme zamanında yaratmasıdır; classsüitte kod nesne (olduğu <code object Foo at 0x10a436030, file "<stdin>", line 2>zaten derlenmektedir).

Sınıf gövdesini oluşturan kod nesnesini inceleyelim; kod nesnelerinin bir co_constsyapısı vardır:

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

Yukarıdaki bayt kodu sınıf gövdesini oluşturur. İşlev yürütülür ve sonuç locals()ad alanı, sınıfı oluşturmak için kullanılır xve ykullanılır (ancak xgenel olarak tanımlanmadığı için çalışmaz ). Saklandıktan sonra bu Not 5olarak x, başka bir kod nesne yükler; liste kavraması bu; sınıf gövdesi gibi bir işlev nesnesine sarılır; oluşturulan işlev range(1), döngüsel kodu için bir yineleyici için kullanılan yinelenebilir bir konum bağımsız değişkeni alır . Bayt kodunda gösterildiği gibi range(1), sınıf kapsamında değerlendirilir.

Buradan, bir işlev veya üretici için bir kod nesnesi ile bir kavrama için bir kod nesnesi arasındaki tek farkın , üst kod nesnesi yürütüldüğünde, ikincisinin hemen yürütülmesidir; bayt kodu basitçe anında bir işlev oluşturur ve bunu birkaç küçük adımda yürütür.

Python 2.x orada satır içi bayt kodu kullanır, burada Python 2.7 çıktısı:

  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        

Kod nesnesi yüklenmedi, bunun yerine bir FOR_ITERdöngü satır içi çalıştırıldı. Yani Python 3.x'te, liste üreticisine kendi kod kodu nesnesi verildi, yani kendi kapsamı var.

Ancak, modül veya komut dosyası yorumlayıcı tarafından ilk kez yüklendiğinde anlama, python kaynak kodunun geri kalanıyla birlikte derlendi ve derleyici bir sınıf paketini geçerli bir kapsam olarak görmüyor . Liste kavrayışında başvurulan tüm değişkenler , sınıf tanımını çevreleyen kapsamı tekrarlamalı olarak incelemelidir. Değişken derleyici tarafından bulunamazsa, bunu genel olarak işaretler. Liste anlama kodu nesnesinin sökülmesi x, gerçekten genel olarak yüklendiğini gösterir :

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

Bu bayt kodu, iletilen ilk bağımsız değişkeni ( range(1)yineleyici) yükler ve tıpkı Python 2.x sürümünün FOR_ITERonu döngüye sokmak ve çıktısını oluşturmak için kullandığı gibi .

Belirlediğimiz olsaydı xiçinde fooişlevin yerine, xbir hücre değişken (hücreler iç içe kapsamlar bakın) olacaktır:

>>> 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_DEREFDolaylı olarak yükleyecek xkod konusu, hücre nesnelerden:

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

Gerçek referans, bir fonksiyon nesnesinin .__closure__özelliğinden başlatılan geçerli çerçeve veri yapılarından gelen değeri arar . Anlama kodu nesnesi için oluşturulan işlev tekrar atıldığından, bu işlevin kapanışını denetleyemiyoruz. Bir kapatma eylemini görmek için bunun yerine iç içe bir işlevi incelemeliyiz:

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

Özetlemek gerekirse:

  • Liste kavrayışları Python 3'te kendi kod nesnelerini alır ve fonksiyonlar, üreteçler veya kavrayışlar için kod nesneleri arasında bir fark yoktur; anlama kodu nesneleri geçici bir işlev nesnesine sarılır ve hemen çağrılır.
  • Kod nesneleri derleme zamanında oluşturulur ve yerel olmayan değişkenler, kodun iç içe kapsamlarına bağlı olarak genel veya serbest değişkenler olarak işaretlenir. Sınıf beden olduğu değil bu değişkenleri arayanlar için bir kapsam düşündü.
  • Kodu yürütürken, Python yalnızca genel olanlara veya o anda yürütülmekte olan nesnenin kapanmasına bakmak zorundadır. Derleyici sınıf gövdesini kapsam olarak içermediğinden, geçici işlev ad alanı dikkate alınmaz.

Geçici çözüm; ya da bu konuda ne yapmalı

Eğer için açık bir kapsam oluşturmak için olsaydı xbir işlevde, şöyle değişken olabilir bir liste anlama için sınıf kapsamlı değişkenleri kullanın:

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

'Geçici' yişlev doğrudan çağrılabilir; dönüş değeri ile yaptığımız zaman değiştiririz. Kapsamı edilir çözme dikkate 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',)

Elbette, kodunuzu okuyan insanlar bunun üzerine biraz kafa çizecek; Buraya neden bunu yaptığınızı açıklayan büyük bir yağ yorumu koymak isteyebilirsiniz.

En iyi çözüm, yalnızca __init__bir örnek değişkeni oluşturmak için kullanmaktır:

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

ve kafa çizmekten ve kendinizi açıklamak için sorulardan kaçının. Kendi somut örneğiniz namedtupleiçin, sınıfı sınıfta bile saklamam ; çıktıyı doğrudan kullanın (oluşturulan sınıfı hiç saklamayın) veya genel bir kullanın:

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

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

21
y = (lambda x=x: [x for i in range(1)])()
Bağlamayı

3
@ecatmur: Kesinlikle, sonuçta lambdasadece anonim işlevlerdir.
Martijn Pieters

2
Kayıt için, sınıf değişkenini iletmek için varsayılan bir bağımsız değişkeni (lambda ya da bir işleve) kullanan geçici çözümde bir gotcha vardır. Yani, değişkenin geçerli değerini geçer . Bu nedenle, değişken daha sonra değişir ve sonra lambda veya işlev çağrılırsa, lambda veya işlev eski değeri kullanır. Bu davranış, bir kapatma davranışından (değerinden ziyade değişkene bir başvuru yakalar) farklıdır, bu nedenle beklenmedik olabilir.
Neal Young

9
Bir şeyin neden sezgisel olarak çalışmadığını açıklamak için bir teknik bilgi sayfası gerektiriyorsa, buna hata diyorum.
Jonathan

5
@JonathanLeaders: Buna böcek deme , denir . A ve B'yi istiyorsanız, ancak bunlardan sadece birini alabilirsiniz, o zaman nasıl karar verirseniz verin, bazı durumlarda sonucu beğenmeyeceksiniz. Hayat bu.
Lutz Prechelt

15

Bence Python 3'te bir kusur var. Umarım değiştirirler.

Old Way (2.7'de çalışır, 3+'de atar NameError: name 'x' is not defined):

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

NOT: sadece kapsamını A.xçözmek onu çözmez

Yeni Yol (3+ sürümde çalışır):

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

Sözdizimi çok çirkin olduğu için, tüm sınıf değişkenlerimi genellikle yapıcıda başlatırım


6
Sorun, Python 2'de, jeneratör ifadelerinin yanı sıra set ve sözlük kavrayışlarında da mevcuttur. Bu bir hata değil, sınıf ad alanlarının nasıl çalıştığının bir sonucudur. Değişmeyecek.
Martijn Pieters

4
Ve geçici çözümünüzün cevabımın tam olarak belirttiği şeyi yaptığını not ediyorum: yeni bir kapsam oluşturun (lambda burada defbir işlev oluşturmak için kullanmaktan farklı değildir ).
Martijn Pieters

1
Evet. Etrafta bir bakışta bir cevap almak güzel olsa da, bu, dilin çalışma şeklinin bir yan etkisi olduğunda (ve bu nedenle değiştirilmeyecek olursa) davranışı bir hata olarak yanlış bir şekilde belirtir.
jsbueno

Bu farklı bir sorundur, aslında Python 3'te bir sorun değildir. IPython'da yalnızca embed modunda say'ı kullanarak çağırdığınızda oluşur python -c "import IPython;IPython.embed()". IPython'u doğrudan say komutunu kullanarak çalıştırın ipythonve sorun ortadan kalkacaktır.
Riaz Rizvi

6

Kabul edilen cevap mükemmel bilgi sağlar, ancak burada başka birkaç kırışıklık var gibi görünüyor - liste kavrayışı ve jeneratör ifadeleri arasındaki farklar. Birlikte oynadığım bir demo:

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

Bu Python'da bir hata. Anlamaların döngülerle eşdeğer olduğu ilan edilir, ancak bu sınıflarda doğru değildir. En azından Python 3.6.6'ya kadar, bir sınıfta kullanılan bir kavramada, kavrama içinde kavrama dışından sadece bir değişken erişilebilir ve en dıştaki yineleyici olarak kullanılmalıdır. Bir işlevde bu kapsam sınırlaması uygulanmaz.

Bunun neden bir hata olduğunu göstermek için orijinal örneğe dönelim. Bu başarısız olur:

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

Ama bu işe yarıyor:

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

Sınırlama, bu kılavuzun sonunda başvuru kılavuzunda belirtilmiştir.


1

En dıştaki yineleyici çevredeki kapsamda değerlendirildiğinden , bağımlılıkları anlama kapsamına taşımak için zipbirlikte kullanabiliriz itertools.repeat:

import itertools as it

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

forAnlamada iç içe döngüler de kullanılabilir ve en dıştaki bağımlılıklara bağımlılıklar dahil edilebilir:

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

OP'nin özel örneği için:

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'],
        # ...
    ])]
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.