Mengakses variabel kelas dari pemahaman daftar dalam definisi kelas


174

Bagaimana Anda mengakses variabel kelas lain dari pemahaman daftar dalam definisi kelas? Berikut ini berfungsi di Python 2 tetapi gagal dalam Python 3:

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

Python 3.2 memberikan kesalahan:

NameError: global name 'x' is not defined

Mencoba Foo.xjuga tidak berhasil. Ada ide tentang cara melakukan ini di Python 3?

Contoh motivasi yang sedikit lebih rumit:

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

Dalam contoh ini, apply()akan menjadi solusi yang layak, tetapi sayangnya dihapus dari Python 3.


Pesan kesalahan Anda salah. Saya NameError: global name 'x' is not definedmenggunakan Python 3.2 dan 3.3 yang saya harapkan.
Martijn Pieters

Menarik ... Satu solusi yang jelas adalah untuk menetapkan y setelah Anda keluar dari definisi kelas. Foo.y = [Foo.x for i in range (1)]
gps

3
+ Martijn-pieters tautan ke duplikat benar, ada komentar dari + matt-b di sana dengan penjelasan: Python 2.7 daftar pemahaman tidak memiliki namespace sendiri (tidak seperti kumpulan atau dict pemahaman atau ekspresi generator ... ganti Anda [ ] dengan {} untuk melihat itu dalam aksi). Mereka semua memiliki namespace mereka sendiri di 3.
gps

@ gps: Atau gunakan lingkup bersarang, dengan menyisipkan fungsi (sementara) di suite definisi kelas.
Martijn Pieters

Saya baru saja menguji pada 2.7.11. Mendapat kesalahan nama
Junchao Gu

Jawaban:


244

Lingkup kelas dan daftar, set atau pemahaman kamus, serta ekspresi generator tidak bercampur.

Itu sebabnya; atau, kata resmi tentang ini

Dalam Python 3, daftar pemahaman diberi ruang lingkup yang tepat (namespace lokal) sendiri, untuk mencegah variabel lokal mereka berdarah ke dalam cakupan sekitarnya (lihat daftar nama-nama rebind pemahaman Python bahkan setelah lingkup pemahaman. Apakah ini benar? ). Itu bagus ketika menggunakan daftar seperti pemahaman dalam modul atau fungsi, tetapi di kelas, pelingkupan sedikit, uhm, aneh .

Ini didokumentasikan dalam pep 227 :

Nama dalam lingkup kelas tidak dapat diakses. Nama diselesaikan di lingkup fungsi terlampir terdalam. Jika definisi kelas terjadi dalam rantai cakupan bersarang, proses resolusi melewatkan definisi kelas.

dan dalam classdokumentasi pernyataan majemuk :

Rangkaian kelas kemudian dieksekusi dalam bingkai eksekusi baru (lihat bagian Penamaan dan penjilidan ), menggunakan namespace lokal yang baru dibuat dan namespace global asli. (Biasanya, suite hanya berisi definisi fungsi.) Ketika suite kelas menyelesaikan eksekusi, frame eksekusi dibuang tetapi namespace lokalnya disimpan . [4] Objek kelas kemudian dibuat menggunakan daftar warisan untuk kelas dasar dan namespace lokal yang disimpan untuk kamus atribut.

Penekanan milikku; kerangka eksekusi adalah ruang lingkup sementara.

Karena ruang lingkup adalah repurposed sebagai atribut pada objek kelas, yang memungkinkannya untuk digunakan sebagai ruang lingkup nonlokal juga mengarah pada perilaku yang tidak terdefinisi; apa yang akan terjadi jika metode kelas disebut xsebagai variabel lingkup bersarang, lalu memanipulasi Foo.xjuga, misalnya? Lebih penting lagi, apa artinya bagi subclass Foo? Python harus memperlakukan ruang lingkup kelas secara berbeda karena sangat berbeda dari ruang lingkup fungsi.

Terakhir, tapi jelas tidak kalah pentingnya, bagian Penamaan dan pengikatan yang tertaut dalam dokumentasi model Eksekusi menyebutkan cakupan kelas secara eksplisit:

Cakupan nama yang didefinisikan dalam blok kelas terbatas pada blok kelas; itu tidak meluas ke blok kode metode - ini termasuk pemahaman dan ekspresi generator karena mereka diimplementasikan menggunakan lingkup fungsi. Ini berarti bahwa yang berikut ini akan gagal:

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

Jadi, untuk meringkas: Anda tidak dapat mengakses ruang lingkup kelas dari fungsi, daftar pemahaman atau ekspresi generator terlampir dalam ruang lingkup itu; mereka bertindak seolah-olah ruang lingkup itu tidak ada. Dalam Python 2, daftar pemahaman diimplementasikan menggunakan cara pintas, tetapi dalam Python 3 mereka mendapat ruang lingkup fungsi mereka sendiri (seperti yang seharusnya mereka lakukan selama ini) dan dengan demikian contoh Anda rusak. Tipe-tipe pemahaman lain memiliki cakupannya sendiri terlepas dari versi Python, jadi contoh serupa dengan himpunan set atau dict akan pecah di Python 2.

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

Pengecualian (kecil); atau, mengapa satu bagian mungkin masih berfungsi

Ada satu bagian dari pemahaman atau ekspresi generator yang dieksekusi di lingkup sekitarnya, terlepas dari versi Python. Itu akan menjadi ekspresi untuk yang terluar iterable. Dalam contoh Anda, ini adalah range(1):

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

Dengan demikian, menggunakan xdalam ekspresi itu tidak akan menimbulkan kesalahan:

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

Ini hanya berlaku untuk yang terluar iterable; jika pemahaman memiliki beberapa forklausa, iterables untuk forklausa dalam dievaluasi dalam lingkup pemahaman:

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

Keputusan desain ini dibuat untuk melemparkan kesalahan pada waktu pembuatan genexp alih-alih waktu iterasi ketika membuat iterable yang paling luar dari ekspresi generator yang melempar kesalahan, atau ketika iterable terluar ternyata tidak menjadi iterable. Pemahaman berbagi perilaku ini untuk konsistensi.

Mencari di bawah tenda; atau, jauh lebih detail daripada yang Anda inginkan

Anda dapat melihat semua ini beraksi menggunakan dismodul . Saya menggunakan Python 3.3 dalam contoh berikut, karena menambahkan nama yang memenuhi syarat yang dengan rapi mengidentifikasi objek kode yang ingin kita periksa. Bytecode yang dihasilkan secara fungsional identik dengan Python 3.2.

Untuk membuat kelas, Python pada dasarnya mengambil seluruh rangkaian yang membentuk tubuh kelas (jadi semuanya menjorok satu tingkat lebih dalam dari class <name>:garis), dan mengeksekusi seolah-olah itu adalah fungsi:

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

Pertama LOAD_CONSTada memuat objek kode untuk Footubuh kelas, kemudian membuatnya menjadi fungsi, dan menyebutnya. The hasil dari panggilan yang kemudian digunakan untuk membuat namespace kelas, yang __dict__. Sejauh ini baik.

Hal yang perlu diperhatikan di sini adalah bahwa bytecode berisi objek kode bersarang; dalam Python, definisi kelas, fungsi, pemahaman, dan generator semuanya direpresentasikan sebagai objek kode yang tidak hanya berisi bytecode, tetapi juga struktur yang mewakili variabel lokal, konstanta, variabel yang diambil dari global, dan variabel yang diambil dari lingkup bersarang. Bytecode yang dikompilasi mengacu pada struktur-struktur tersebut dan interpreter python tahu bagaimana mengakses yang diberikan bytecode yang disajikan.

Yang penting untuk diingat di sini adalah bahwa Python membuat struktur ini pada waktu kompilasi; yang classsuite benda kode ( <code object Foo at 0x10a436030, file "<stdin>", line 2>) yang sudah disusun.

Mari kita periksa objek kode yang membuat tubuh kelas itu sendiri; objek kode memiliki 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         

Bytecode di atas menciptakan tubuh kelas. Fungsi dieksekusi dan locals()namespace yang dihasilkan , berisi xdan ydigunakan untuk membuat kelas (kecuali bahwa itu tidak berfungsi karena xtidak didefinisikan sebagai global). Perhatikan bahwa setelah menyimpan 5di x, itu beban kode objek lain; itulah pemahaman daftar; itu dibungkus dalam objek fungsi seperti tubuh kelas itu; fungsi yang dibuat mengambil argumen posisi, range(1)iterable untuk digunakan untuk kode pengulangan, dilemparkan ke sebuah iterator. Seperti yang ditunjukkan dalam bytecode, range(1)dievaluasi dalam lingkup kelas.

Dari sini Anda dapat melihat bahwa satu-satunya perbedaan antara objek kode untuk fungsi atau generator, dan objek kode untuk pemahaman adalah bahwa yang terakhir dieksekusi segera ketika objek kode induk dijalankan; bytecode hanya menciptakan fungsi dengan cepat dan menjalankannya dalam beberapa langkah kecil.

Python 2.x menggunakan inline bytecode sebagai gantinya, berikut adalah output dari 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        

Tidak ada objek kode yang dimuat, sebaliknya sebuah FOR_ITERloop dijalankan inline. Jadi dalam Python 3.x, generator daftar diberi objek kode yang tepat sendiri, yang artinya memiliki ruang lingkup sendiri.

Namun, pemahaman dikompilasi bersama-sama dengan sisa kode sumber python ketika modul atau skrip pertama kali dimuat oleh interpreter, dan kompiler tidak menganggap suite kelas lingkup yang valid. Setiap variabel yang direferensikan dalam pemahaman daftar harus melihat dalam lingkup seputar definisi kelas, secara rekursif. Jika variabel tidak ditemukan oleh kompiler, itu menandainya sebagai global. Pembongkaran objek kode pemahaman daftar menunjukkan bahwa xmemang dimuat sebagai 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         

Potongan bytecode ini memuat argumen pertama yang dilewatkan ( range(1)iterator), dan seperti versi Python 2.x yang digunakan FOR_ITERuntuk mengulanginya dan membuat outputnya.

Seandainya kita mendefinisikan xdalam foofungsi sebagai gantinya, xakan menjadi variabel sel (sel merujuk pada cakupan bersarang):

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

The LOAD_DEREFsecara tidak langsung akan memuat xdari benda sel kode objek:

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

Referensi aktual terlihat nilainya dari struktur data frame saat ini, yang diinisialisasi dari .__closure__atribut objek fungsi . Karena fungsi yang dibuat untuk objek kode pemahaman dibuang lagi, kita tidak bisa memeriksa penutupan fungsi itu. Untuk melihat penutupan dalam aksi, kita harus memeriksa fungsi bersarang sebagai gantinya:

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

Jadi, untuk meringkas:

  • Daftar pemahaman mendapatkan objek kode mereka sendiri di Python 3, dan tidak ada perbedaan antara objek kode untuk fungsi, generator atau pemahaman; objek kode pemahaman dibungkus dalam objek fungsi sementara dan dipanggil segera.
  • Objek kode dibuat pada waktu kompilasi, dan variabel non-lokal ditandai sebagai variabel global atau bebas, berdasarkan cakupan kode yang bersarang. Badan kelas tidak dianggap sebagai ruang lingkup untuk mencari variabel-variabel tersebut.
  • Saat mengeksekusi kode, Python hanya perlu melihat ke dalam global, atau penutupan objek yang sedang dieksekusi. Karena kompiler tidak menyertakan tubuh kelas sebagai ruang lingkup, fungsi sementara namespace tidak dipertimbangkan.

Penanganan masalah; atau, apa yang harus dilakukan?

Jika Anda membuat ruang lingkup eksplisit untuk xvariabel, seperti dalam suatu fungsi, Anda bisa menggunakan variabel ruang lingkup untuk pemahaman daftar:

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

Fungsi 'sementara' ydapat dipanggil langsung; kita menggantinya ketika kita lakukan dengan nilai pengembaliannya. Ruang lingkup yang dipertimbangkan ketika menyelesaikan 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',)

Tentu saja, orang yang membaca kode Anda akan menggaruk-garuk kepala ini sedikit; Anda mungkin ingin memberikan komentar besar di sana menjelaskan mengapa Anda melakukan ini.

Cara terbaik untuk menyelesaikannya adalah dengan hanya menggunakan __init__untuk membuat variabel instan sebagai gantinya:

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

dan hindari semua yang menggaruk kepala, dan pertanyaan untuk menjelaskan diri sendiri. Sebagai contoh konkret Anda sendiri, saya bahkan tidak akan menyimpannya namedtupledi kelas; baik menggunakan output secara langsung (jangan menyimpan kelas yang dihasilkan sama sekali), atau menggunakan global:

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

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

21
Anda juga dapat menggunakan lambda untuk memperbaiki ikatan:y = (lambda x=x: [x for i in range(1)])()
ecatmur

3
@ecatmur: Sebenarnya, lambdaitu hanya fungsi anonim.
Martijn Pieters

2
Sebagai catatan, penyelesaian yang menggunakan argumen default (ke lambda atau fungsi) untuk lulus dalam variabel kelas memiliki gotcha. Yaitu, ia melewati nilai variabel saat ini. Jadi, jika variabel berubah nanti, dan kemudian lambda atau fungsi dipanggil, lambda atau fungsi akan menggunakan nilai lama. Perilaku ini berbeda dari perilaku penutupan (yang akan menangkap referensi ke variabel, bukan nilainya), jadi mungkin tidak terduga.
Neal Young

9
Jika memerlukan halaman informasi teknis untuk menjelaskan mengapa sesuatu tidak bekerja secara intuitif, saya menyebutnya bug.
Jonathan

5
@ JonathanLeaders: Jangan menyebutnya bug , sebut itu tradeoff . Jika Anda menginginkan A dan B, tetapi hanya bisa mendapatkan salah satunya, maka tidak masalah bagaimana Anda memutuskan, dalam beberapa situasi Anda tidak akan menyukai hasilnya. Itulah hidup.
Lutz Prechelt

15

Menurut pendapat saya itu adalah cacat dalam Python 3. Saya harap mereka mengubahnya.

Old Way (bekerja di 2.7, melempar NameError: name 'x' is not defined3+):

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

CATATAN: hanya dengan scoping tidak A.xakan menyelesaikannya

Cara Baru (berfungsi dalam 3+):

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

Karena sintaksnya sangat jelek, saya hanya menginisialisasi semua variabel kelas saya di konstruktor


6
Masalahnya ada di Python 2 juga, ketika menggunakan ekspresi generator, serta dengan set dan pemahaman kamus. Ini bukan bug, itu adalah konsekuensi dari bagaimana ruang nama kelas bekerja. Itu tidak akan berubah.
Martijn Pieters

4
Dan saya perhatikan bahwa solusi Anda melakukan apa yang sudah dinyatakan oleh jawaban saya: buat cakupan baru (lambda tidak berbeda dengan menggunakan defuntuk membuat fungsi).
Martijn Pieters

1
ya. Meskipun menyenangkan untuk memiliki jawaban dengan latihan sekilas, yang satu ini dengan tidak tepat menyatakan perilaku sebagai bug, ketika itu adalah efek samping dari cara kerja bahasa (dan karena itu, tidak akan diubah)
jsbueno

Ini adalah masalah yang berbeda, yang sebenarnya bukan masalah di Python 3. Ini hanya terjadi di IPython ketika Anda menyebutnya dalam mode embed menggunakan say python -c "import IPython;IPython.embed()". Jalankan IPython secara langsung menggunakan katakan ipythondan masalahnya akan hilang.
Riaz Rizvi

6

Jawaban yang diterima memberikan informasi yang sangat baik, tetapi tampaknya ada beberapa kerutan di sini - perbedaan antara pemahaman daftar dan ekspresi generator. Demo yang saya mainkan:

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

Ini adalah bug di Python. Pemahaman diiklankan sebagai setara dengan untuk loop, tetapi ini tidak benar di kelas. Setidaknya hingga Python 3.6.6, dalam pemahaman yang digunakan dalam kelas, hanya satu variabel dari luar pemahaman yang dapat diakses di dalam pemahaman, dan itu harus digunakan sebagai iterator terluar. Dalam suatu fungsi, batasan ruang lingkup ini tidak berlaku.

Untuk mengilustrasikan mengapa ini bug, mari kita kembali ke contoh aslinya. Ini gagal:

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

Tapi ini berhasil:

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

Batasan tersebut dinyatakan di akhir bagian ini dalam panduan referensi.


1

Karena iterator terluar dievaluasi dalam lingkup sekitar kita dapat menggunakan zipbersama-sama itertools.repeatuntuk membawa dependensi ke ruang lingkup pemahaman:

import itertools as it

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

Satu juga dapat menggunakan forloop bersarang dalam pemahaman dan menyertakan dependensi di terluar iterable:

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

Untuk contoh spesifik 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'],
        # ...
    ])]
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.