Pemahaman daftar mengembalikan nama bahkan setelah cakupan pemahaman. Apakah ini benar?

118

Pemahaman mengalami beberapa interaksi tak terduga dengan pelingkupan. Apakah ini perilaku yang diharapkan?

Saya punya metode:

def leave_room(self, uid):
  u = self.user_by_id(uid)
  r = self.rooms[u.rid]

  other_uids = [ouid for ouid in r.users_by_id.keys() if ouid != u.uid]
  other_us = [self.user_by_id(uid) for uid in other_uids]

  r.remove_user(uid) # OOPS! uid has been re-bound by the list comprehension above

  # Interestingly, it's rebound to the last uid in the list, so the error only shows
  # up when len > 1

Dengan risiko merengek, ini adalah sumber kesalahan yang brutal. Saat saya menulis kode baru, saya hanya sesekali menemukan kesalahan yang sangat aneh karena pengikatan ulang - bahkan sekarang saya tahu itu masalah. Saya perlu membuat aturan seperti "selalu awali temp vars dalam pemahaman daftar dengan garis bawah", tetapi itu pun bukan bukti yang bodoh.

Fakta bahwa ada semacam menunggu bom waktu acak ini meniadakan semua "kemudahan penggunaan" dari pemahaman daftar.

Jabavu Adams
sumber
7
-1: "sumber kesalahan yang brutal"? Hampir tidak. Mengapa memilih istilah argumentatif seperti itu? Umumnya kesalahan yang paling mahal adalah kesalahpahaman persyaratan dan kesalahan logika sederhana. Jenis kesalahan ini telah menjadi masalah standar di banyak bahasa pemrograman. Mengapa menyebutnya 'brutal'?
S. Lott
44
Itu melanggar prinsip paling tidak mengejutkan. Ini juga tidak disebutkan dalam dokumentasi python tentang pemahaman daftar yang bagaimanapun menyebutkan beberapa kali betapa mudah dan nyamannya mereka. Pada dasarnya ini adalah ranjau darat yang ada di luar model bahasa saya, dan karenanya tidak mungkin saya perkirakan.
Jabavu Adams
33
1 untuk "sumber kesalahan brutal". Kata 'brutal' sepenuhnya bisa dibenarkan.
Nathaniel
3
Satu-satunya hal "brutal" yang saya lihat di sini adalah konvensi penamaan Anda. Ini bukan tahun 80-an lagi Anda tidak terbatas pada 3 nama variabel karakter.
UloPe
5
Catatan: dokumentasi yang tidak menyatakan bahwa daftar-pemahaman yang setara dengan eksplisit formembangun -loop dan for-loops variabel kebocoran . Jadi itu tidak eksplisit tetapi dinyatakan secara implisit.
Bakuriu

Jawaban:

172

Pemahaman daftar membocorkan variabel kontrol loop di Python 2 tetapi tidak di Python 3. Berikut Guido van Rossum (pencipta Python) yang menjelaskan sejarah di balik ini:

Kami juga membuat perubahan lain di Python 3, untuk meningkatkan kesetaraan antara pemahaman daftar dan ekspresi generator. Di Python 2, pemahaman daftar "membocorkan" variabel kontrol loop ke lingkup sekitarnya:

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'

Ini adalah artefak dari implementasi asli dari pemahaman daftar; itu adalah salah satu "rahasia kecil kotor" Python selama bertahun-tahun. Ini dimulai sebagai kompromi yang disengaja untuk membuat pemahaman daftar dengan sangat cepat, dan meskipun ini bukan kesalahan umum bagi pemula, hal itu terkadang menyengat orang. Untuk ekspresi generator, kami tidak dapat melakukan ini. Ekspresi generator diimplementasikan menggunakan generator, yang eksekusinya memerlukan kerangka eksekusi terpisah. Jadi, ekspresi generator (terutama jika mereka mengulang dalam urutan pendek) kurang efisien dibandingkan pemahaman daftar.

Namun, di Python 3, kami memutuskan untuk memperbaiki "rahasia kecil kotor" dari pemahaman daftar dengan menggunakan strategi implementasi yang sama seperti untuk ekspresi generator. Jadi, dalam Python 3, contoh di atas (setelah modifikasi menggunakan print (x) :-) akan mencetak 'sebelum', membuktikan bahwa 'x' dalam pemahaman daftar untuk sementara membayangi tetapi tidak menimpa 'x' di sekitarnya cakupan.

Steven Rumbalski
sumber
14
Saya akan menambahkan bahwa meskipun Guido menyebutnya sebagai "rahasia kecil yang kotor", banyak yang menganggapnya sebagai fitur, bukan bug.
Steven Rumbalski
38
Perhatikan juga bahwa sekarang di 2.7, pemahaman set dan kamus (dan generator) memiliki cakupan pribadi, tetapi pemahaman daftar masih tidak. Meskipun ini masuk akal karena yang pertama semuanya di-back-port dari Python 3, itu benar-benar membuat kontras dengan pemahaman daftar yang menggelegar.
Matt B.
7
Saya tahu ini adalah pertanyaan yang sangat kuno, tetapi mengapa beberapa orang menganggapnya sebagai ciri bahasa? Apakah ada yang mendukung kebocoran variabel semacam ini?
Mathias Müller
2
karena: loop bocor memiliki alasan bagus, esp. untuk mengakses nilai terakhir setelah awal break- tetapi tidak relevan dengan pemahaman. Saya ingat beberapa diskusi comp.lang.python di mana orang ingin menetapkan variabel di tengah ekspresi. Cara yang kurang gila yang ditemukan adalah nilai tunggal untuk klausa mis. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1], tetapi hanya membutuhkan pemahaman-lokal var dan bekerja dengan baik di Python 3. Saya pikir "bocor" adalah satu-satunya cara untuk mengatur variabel terlihat di luar ekspresi. Semua orang setuju bahwa teknik ini mengerikan :-)
Beni Cherniavsky-Paskin
1
Masalahnya di sini bukanlah memiliki akses ke cakupan sekitar pemahaman daftar, tetapi mengikat dalam cakupan pemahaman daftar yang memengaruhi cakupan sekitarnya.
Felipe Gonçalves Marques
48

Ya, daftar pemahaman "membocorkan" variabel mereka dengan Python 2.x, seperti untuk loop.

Dalam retrospeksi, ini dikenali sebagai kesalahan, dan itu dihindari dengan ekspresi generator. EDIT: Seperti yang dicatat Matt B. itu juga dihindari ketika sintaks pemahaman set dan kamus di-backport dari Python 3.

Perilaku pemahaman daftar harus dibiarkan seperti pada Python 2, tetapi sepenuhnya diperbaiki dalam Python 3.

Ini berarti bahwa di semua:

list(x for x in a if x>32)
set(x//4 for x in a if x>32)         # just another generator exp.
dict((x, x//16) for x in a if x>32)  # yet another generator exp.
{x//4 for x in a if x>32}            # 2.7+ syntax
{x: x//16 for x in a if x>32}        # 2.7+ syntax

the xselalu lokal untuk ekspresi sementara ini:

[x for x in a if x>32]
set([x//4 for x in a if x>32])         # just another list comp.
dict([(x, x//16) for x in a if x>32])  # yet another list comp.

dengan Python 2.x semua membocorkan xvariabel ke lingkup sekitarnya.


UPDATE untuk Python 3.8 (?) : PEP 572 akan memperkenalkan :=operator penugasan yang sengaja bocor keluar dari pemahaman dan ekspresi generator! Ini pada dasarnya dimotivasi oleh 2 kasus penggunaan: menangkap "saksi" dari fungsi penghentian awal seperti any()dan all():

if any((comment := line).startswith('#') for line in lines):
    print("First comment:", comment)
else:
    print("There are no comments")

dan memperbarui status yang bisa berubah:

total = 0
partial_sums = [total := total + v for v in values]

Lihat Lampiran B untuk pelingkupan yang tepat. Variabel ditempatkan di sekitar terdekat defatau lambda, kecuali fungsi itu mendeklarasikannya nonlocalatau global.

Beni Cherniavsky-Paskin
sumber
7

Ya, penugasan dilakukan di sana, seperti yang akan terjadi di file for putaran. Tidak ada cakupan baru yang sedang dibuat.

Ini jelas merupakan perilaku yang diharapkan: pada setiap siklus, nilainya terikat pada nama yang Anda tentukan. Misalnya,

>>> x=0
>>> a=[1,54,4,2,32,234,5234,]
>>> [x for x in a if x>32]
[54, 234, 5234]
>>> x
5234

Setelah dikenali, tampaknya cukup mudah untuk dihindari: jangan gunakan nama yang ada untuk variabel dalam pemahaman.

JAL
sumber
2

Menariknya, ini tidak memengaruhi kamus atau pemahaman himpunan.

>>> [x for x in range(1, 10)]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> x
9
>>> {x for x in range(1, 5)}
set([1, 2, 3, 4])
>>> x
9
>>> {x:x for x in range(1, 100)}
{1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99}
>>> x
9

Namun telah diperbaiki dalam 3 seperti disebutkan di atas.

Chris Travers
sumber
Sintaks itu tidak bekerja sama sekali di Python 2.6. Apakah Anda berbicara tentang Python 2.7?
Paul Hollingsworth
Python 2.6 hanya memiliki pemahaman daftar seperti halnya Python 3.0. 3.1 menambahkan set dan pemahaman kamus dan ini porting ke 2.7. Maaf kalau belum jelas. Itu dimaksudkan untuk mencatat batasan pada jawaban lain, dan versi mana yang diterapkan tidak sepenuhnya langsung.
Chris Travers
Meskipun saya dapat membayangkan membuat argumen bahwa ada kasus di mana menggunakan python 2.7 untuk kode baru masuk akal, saya tidak dapat mengatakan hal yang sama untuk python 2.6 ... Bahkan jika 2.6 adalah yang disertakan dengan OS Anda, Anda tidak terjebak dengan Itu. Pertimbangkan untuk menginstal virtualenv dan menggunakan 3.6 untuk kode baru!
Alex L
Poin tentang Python 2.6 bisa muncul dalam mempertahankan sistem lama yang ada. Jadi sebagai catatan sejarah, hal itu tidak sepenuhnya tidak relevan. Sama dengan 3.0 (ick)
Chris Travers
Maaf jika saya terdengar kasar, tetapi ini tidak menjawab pertanyaan dengan cara apa pun. Ini lebih cocok sebagai komentar.
0xc0de
1

beberapa solusi, untuk python 2.6, jika perilaku ini tidak diinginkan

# python
Python 2.6.6 (r266:84292, Aug  9 2016, 06:11:56)
Type "help", "copyright", "credits" or "license" for more information.
>>> x=0
>>> a=list(x for x in xrange(9))
>>> x
0
>>> a=[x for x in xrange(9)]
>>> x
8
Marek Slebodnik
sumber
-1

Di python3 sementara dalam pemahaman daftar, variabel tidak mendapatkan perubahan setelah cakupannya berakhir tetapi ketika kita menggunakan perulangan sederhana variabel tersebut dipindahkan ke luar cakupan.

i = 1 cetakan (i) cetakan ([i dalam kisaran (5)]) cetakan (i) Nilai i akan tetap 1 saja.

Sekarang gunakan hanya untuk loop nilai i akan ditugaskan kembali.

TANYAKAN KUMAR
sumber