Saya menjalankan potongan kode yang tiba-tiba memberikan sebuah kesalahan logika pada salah satu bagian dari program. Ketika menyelidiki bagian, saya membuat sebuah file tes untuk menguji mengatur pernyataan yang dijalankan dan menemukan sebuah bug yang tidak biasa yang tampaknya sangat aneh.
Saya diuji ini kode sederhana:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs filtered
Dan output adalah:
>>> []
Ya, tidak ada. Saya mengharapkan filter pemahaman untuk mendapatkan item dalam array dengan hitungan 2 dan output ini, tapi aku tidak't mendapatkan bahwa:
# Expected output
>>> [2, 2]
Ketika saya berkomentar di luar baris ketiga untuk menguji sekali lagi:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
### array = [5, 6, 1, 2, 9] # Ignore line
print(list(f)) # Outputs filtered
Output adalah benar (anda dapat menguji untuk diri sendiri):
>>> [2, 2]
Pada satu titik saya dikeluarkan jenis variabel f
:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original
print(type(f))
print(list(f)) # Outputs filtered
Dan saya punya:
>>> <class 'generator'>
>>> []
Mengapa memperbarui daftar di Python mengubah output dari generator lain variabel? Hal ini tampaknya sangat aneh bagi saya.
Python's generator ekspresi ikatan (lihat PEP 289 -- Generator Ekspresi) (apa jawaban yang lain menyebutnya "malas"):
Ikatan terhadap Ikatan
Setelah banyak diskusi, diputuskan bahwa yang pertama (terluar) untuk ekspresi [generator ekspresi] harus dievaluasi segera dan yang tersisa ekspresi dievaluasi ketika generator dijalankan.
[...] Python membutuhkan ikatan pendekatan ekspresi lambda dan tidak memiliki preseden untuk otomatis, awal mengikat. Ia merasa bahwa memperkenalkan paradigma baru yang tidak perlu akan memperkenalkan kompleksitas.
Setelah menjelajahi banyak kemungkinan, sebuah konsensus muncul yang mengikat isu-isu yang sulit untuk memahami dan bahwa pengguna harus sangat dianjurkan untuk menggunakan generator ekspresi dalam fungsi yang mengkonsumsi argumen mereka segera. Untuk aplikasi yang lebih kompleks, penuh generator definisi yang selalu unggul dalam hal yang jelas tentang ruang lingkup, seumur hidup, dan mengikat.
Itu artinya hanya mengevaluasi terluar untuk
saat membuat generator ekspresi. Jadi itu benar-benar mengikat nilai dengan nama menginap
di "subexpressionn" dalam array
(sebenarnya itu's mengikat setara dengan iter(array)
pada saat ini). Tapi ketika anda iterate atas generator jika array.menghitung
panggilan sebenarnya mengacu pada apa yang saat ini bernama array
.
Sejak itu's benar-benar sebuah daftar
tidak array
saya mengubah nama-nama variabel di sisa jawabannya menjadi lebih akurat.
Dalam kasus pertama daftar
anda iterate atas dan daftar
anda menghitung dalam akan berbeda. It's seperti jika anda digunakan:
list1 = [1, 2, 2, 4, 5]
list2 = [5, 6, 1, 2, 9]
f = (x for x in list1 if list2.count(x) == 2)
Jadi anda memeriksa untuk setiap elemen dalam list1
jika dihitung dalam list2
adalah dua.
Anda dapat dengan mudah memverifikasi ini dengan memodifikasi daftar kedua:
>>> lst = [1, 2, 2]
>>> f = (x for x in lst if lst.count(x) == 2)
>>> lst = [1, 1, 2]
>>> list(f)
[1]
Jika iterasi lebih dari daftar pertama dan dihitung dalam daftar pertama itu akan've kembali [2, 2]
(karena daftar pertama berisi dua 2
). Jika ia mengulangi lagi dan dihitung dalam daftar kedua output harus [1, 1]
. Tapi sejak itu iterates atas daftar pertama (yang mengandung satu 1
) tapi cek daftar kedua (yang berisi dua 1) output adalah salah satu
1`.
Ada beberapa solusi yang mungkin, saya biasanya memilih untuk tidak menggunakan "generator ekspresi" jika mereka tidak't iterasi berakhir segera. Sederhana generator fungsi akan cukup untuk membuatnya bekerja dengan benar:
def keep_only_duplicated_items(lst):
for item in lst:
if lst.count(item) == 2:
yield item
Dan kemudian menggunakannya seperti ini:
lst = [1, 2, 2, 4, 5]
f = keep_only_duplicated_items(lst)
lst = [5, 6, 1, 2, 9]
>>> list(f)
[2, 2]
Perhatikan bahwa PEP (lihat link di atas) juga menyatakan bahwa untuk sesuatu yang lebih rumit penuh generator definisi yang lebih disukai.
Solusi yang lebih baik (menghindari kuadrat runtime perilaku karena anda iterate atas seluruh array untuk setiap elemen dalam array) akan menghitung (koleksi.Counter
) unsur-unsur sekali dan kemudian melakukan pencarian di waktu yang konstan (dihasilkan dalam waktu linier):
from collections import Counter
def keep_only_duplicated_items(lst):
cnts = Counter(lst)
for item in lst:
if cnts[item] == 2:
yield item
It's cukup mudah untuk membuat daftar
subclass yang mencetak ketika metode-metode khusus yang disebut, sehingga seseorang dapat memverifikasi bahwa itu benar-benar bekerja seperti itu.
Dalam hal ini saya hanya menimpa metode __iter__
dan menghitung
karena saya'm tertarik lebih dari yang daftar generator ekspresi iterates dan dalam daftar itu penting. Metode tubuh sebenarnya hanya mendelegasikan ke superclass dan mencetak sesuatu (karena menggunakan super
tanpa argumen dan f-string ini membutuhkan Python 3.6 tetapi harus mudah beradaptasi untuk Python versi):
class MyList(list):
def __iter__(self):
print(f'__iter__() called on {self!r}')
return super().__iter__()
def count(self, item):
cnt = super().count(item)
print(f'count({item!r}) called on {self!r}, result: {cnt}')
return cnt
Ini adalah sederhana subclass hanya mencetak ketika __iter__
dan menghitung
metode yang disebut:
>>> lst = MyList([1, 2, 2, 4, 5])
>>> f = (x for x in lst if lst.count(x) == 2)
__iter__() called on [1, 2, 2, 4, 5]
>>> lst = MyList([5, 6, 1, 2, 9])
>>> print(list(f))
count(1) called on [5, 6, 1, 2, 9], result: 1
count(2) called on [5, 6, 1, 2, 9], result: 1
count(2) called on [5, 6, 1, 2, 9], result: 1
count(4) called on [5, 6, 1, 2, 9], result: 0
count(5) called on [5, 6, 1, 2, 9], result: 1
[]
Seperti orang lain telah disebutkan generator Python adalah malas. Ketika baris ini dijalankan:
f = (x for x in array if array.count(x) == 2) # Filters original
tidak ada yang benar-benar terjadi namun. Anda've hanya menyatakan bagaimana generator fungsi f akan bekerja. Array tidak melihat belum. Maka, anda membuat array baru yang menggantikan yang pertama, dan akhirnya ketika anda menelepon
print(list(f)) # Outputs filtered
generator sekarang membutuhkan nilai yang sebenarnya dan mulai menarik mereka dari generator f. Tapi pada titik ini, hotel yang sudah mengacu pada yang kedua, sehingga anda mendapatkan daftar kosong.
Jika anda perlu untuk menetapkan kembali daftar, dan dapat't menggunakan variabel yang berbeda untuk menahannya, pertimbangkan untuk membuat daftar bukan generator di baris kedua:
f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)
Orang lain sudah menjelaskan akar penyebab dari masalah - generator mengikat untuk nama array
variabel lokal, bukan nilainya.
Yang paling pythonic solusi pasti daftar pemahaman:
f = [x for x in array if array.count(x) == 2]
Namun, jika ada beberapa alasan bahwa anda don't ingin membuat daftar, anda ** bisa juga memaksa lingkup close lebih dari array
:
f = (lambda array=array: (x for x in array if array.count(x) == 2))()
Apa's terjadi di sini adalah bahwa lambda
menangkap referensi untuk menginap
di waktu garis berjalan, memastikan bahwa generator melihat variabel yang anda harapkan, bahkan jika variabel ini kemudian didefinisikan ulang.
Catatan bahwa ini masih mengikat variabel (referensi), bukan nilai**, jadi, sebagai contoh, berikut akan mencetak[2, 2, 4, 4]
:
array = [1, 2, 2, 4, 5] # Original array
f = (lambda array=array: (x for x in array if array.count(x) == 2))() # Close over array
array.append(4) # This *will* be captured
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs [2, 2, 4, 4]
Ini adalah pola yang umum dalam beberapa bahasa, tetapi itu's sangat tidak pythonic, jadi hanya benar-benar masuk akal jika ada's sebuah alasan yang sangat baik untuk tidak menggunakan daftar pemahaman (misalnya, jika array
sangat panjang, atau sedang digunakan di bersarang generator pemahaman, dan anda're khawatir tentang memori).
Anda tidak menggunakan generator dengan benar jika ini adalah penggunaan utama dari kode ini. Menggunakan daftar pemahaman bukannya generator pemahaman. Hanya mengganti tanda kurung dengan tanda kurung. Mengevaluasi ke daftar jika anda don't tahu.
array = [1, 2, 2, 4, 5]
f = [x for x in array if array.count(x) == 2]
array = [5, 6, 1, 2, 9]
print(f)
#[2, 2]
Anda mendapatkan respon ini karena sifat dari generator. Anda're memanggil generator ketika itu't isi akan mengevaluasi untuk []
Generator yang malas, mereka tidak't dapat dievaluasi sampai anda iterate melalui mereka. Dalam hal ini yang's pada titik anda membuat daftar
dengan generator sebagai input, di print
.
Akar penyebab dari masalah ini adalah bahwa generator yang malas; variabel-variabel yang dievaluasi setiap saat:
``
l = [1, 2, 2, 4, 5, 5, 5] disaring = (x x l l jika.count(x) == 2) l = [1, 2, 4, 4, 5, 6, 6] daftar(disaring) [4] ``
Itu iterates dari daftar asli dan mengevaluasi kondisi dengan daftar saat ini. Dalam kasus ini, 4 muncul dua kali dalam daftar baru, menyebabkan itu untuk muncul dalam hasil. Itu hanya muncul sekali dalam hasil karena hanya muncul sekali dalam daftar asli. 6s muncul dua kali dalam daftar baru, tetapi tidak pernah muncul dalam daftar lama dan karenanya tidak pernah ditampilkan.
Fungsi penuh introspeksi bagi yang penasaran (garis dengan komentar yang penting line):
``
l = [1, 2, 2, 4, 5] disaring = (x x l l jika.count(x) == 2) l = [1, 2, 4, 4, 5, 6, 6] daftar(disaring) [4] def f(asli, baru, count): saat ini = asli disaring = (x untuk x di saat ini jika saat ini.count(x) == count) saat ini = new kembali daftar(disaring)
dari dis impor dis dis(f) 2 0 LOAD_FAST 0 (asli) 3 STORE_DEREF 1 (saat ini)
3 6 LOAD_CLOSURE 0 (menghitung)
9 LOAD_CLOSURE 1 (saat ini)
12 BUILD_TUPLE 2
15 LOAD_CONST 1 (<kode objek
4 34 LOAD_FAST 1 (baru) 37 STORE_DEREF 1 (saat ini)
5 40 LOAD_GLOBAL 0 (klik disini) 43 LOAD_FAST 3 (disaring) 46 CALL_FUNCTION 1 (1 posisional, 0 kata kunci sepasang) 49 RETURN_VALUE
f.kode.co_varnames ('original', 'baru', 'menghitung', 'disaring') f.kode.co_cellvars ('menghitung', 'saat') f.kode.co_consts (Tidak ada, <kode objek
di 0x02DD36B0, file "<pyshell#17>", baris 3>, 'f. . ') f.kode.co_consts[1] <kode objek di 0x02DD36B0, file "<pyshell#17>", baris 3> dis(f.kode.co_consts[1]) 3 0 LOAD_FAST 0 (.0) 3 FOR_ITER 32 (38) 6 STORE_FAST 1 (x) 9 LOAD_DEREF 1 (saat ini) # Ini memuat daftar saat setiap waktu, sebagai lawan untuk pembebanan konstan. 12 LOAD_ATTR 0 (menghitung) 15 LOAD_FAST 1 (x) 18 CALL_FUNCTION 1 (1 posisional, 0 kata kunci sepasang) 21 LOAD_DEREF 0 (menghitung) 24 COMPARE_OP 2 (==) 27 POP_JUMP_IF_FALSE 3 30 LOAD_FAST 1 (x) 33 YIELD_VALUE 34 POP_TOP 35 JUMP_ABSOLUTE 3 38 LOAD_CONST 0 (Tidak ada) 41 RETURN_VALUE f.kode.co_consts[1].co_consts (Tidak ada) ``
Untuk mengulangi: daftar untuk iterasi hanya dimuat sekali. Setiap penutupan dalam kondisi atau ekspresi, namun, yang dimuat dari melampirkan lingkup masing-masing iterasi. Mereka tidak disimpan di sebuah konstan.
Solusi terbaik untuk masalah anda ini akan membuat variabel baru referensi daftar aslinya dan gunakan itu dalam generator ekspresi,.
Generator malas dan anda baru ditetapkan array
digunakan ketika anda buang generator anda setelah penyesuaian. Oleh karena itu, output yang benar. Perbaikan cepat adalah dengan menggunakan daftar pemahaman dengan mengganti tanda kurung ()
oleh tanda kurung siku []
.
Pindah ke cara yang lebih baik untuk menulis logika anda, menghitung nilai dalam lingkaran kuadrat memiliki kompleksitas. Untuk algoritma yang bekerja pada linear waktu, anda dapat menggunakan koleksi.Counter
untuk menghitung nilai-nilai, dan menyimpan salinan asli anda klik disini:
from collections import Counter
array = [1, 2, 2, 4, 5] # original array
counts = Counter(array) # count each value in array
old_array = array.copy() # make copy
array = [5, 6, 1, 2, 9] # updates array
# order relevant
res = [x for x in old_array if counts[x] >= 2]
print(res)
# [2, 2]
# order irrelevant
from itertools import chain
res = list(chain.from_iterable([x]*count for x, count in counts.items() if count >= 2))
print(res)
# [2, 2]
Melihat kedua versi doesn't bahkan memerlukan old_array
dan lebih berguna jika tidak ada kebutuhan untuk mempertahankan memesan dari nilai-nilai dalam array asli.
Generator evaluasi adalah "malas" -- itu doesn't mendapatkan dieksekusi sampai anda mewujudkannya dengan referensi yang tepat. Dengan baris anda:
Melihat kembali pada output dengan tipe f
: bahwa objek adalah generator, tidak berurutan. It's menunggu untuk digunakan, sebuah iterator macam.
Generator anda isn't dievaluasi sampai anda mulai memerlukan nilai-nilai dari itu. Pada saat itu, ia menggunakan nilai-nilai yang tersedia saat itu, tidak titik di mana ia didefinisikan.
Kode untuk "membuatnya bekerja"
Itu tergantung pada apa yang anda maksud dengan "membuatnya bekerja". Jika anda ingin f
untuk menjadi daftar disaring, kemudian gunakan daftar, tidak generator:
f = [x for x in array if array.count(x) == 2] # Filters original