Memodifikasi dikt Python saat mengulanginya

87

Katakanlah kita memiliki kamus Python d, dan kita mengulanginya seperti ini:

for k,v in d.iteritems():
    del d[f(k)] # remove some item
    d[g(k)] = v # add a new item

( fdan ghanya beberapa transformasi kotak hitam.)

Dengan kata lain, kami mencoba menambah / menghapus item ke dsaat iterasi menggunakan iteritems.

Apakah ini didefinisikan dengan baik? Bisakah Anda memberikan beberapa referensi untuk mendukung jawaban Anda?

(Sangat jelas bagaimana cara memperbaikinya jika rusak, jadi ini bukan sudut yang saya cari.)

NPE
sumber
Saya telah mencoba melakukan ini dan tampaknya jika Anda membiarkan ukuran dict awal tidak berubah - misalnya mengganti kunci / nilai apa pun alih-alih menghapusnya maka kode ini tidak akan membuang pengecualian
Artsiom Rudzenka
Saya tidak setuju bahwa "cukup jelas bagaimana cara memperbaikinya jika rusak" untuk semua orang yang mencari topik ini (termasuk saya), dan saya berharap jawaban yang diterima setidaknya menyentuh ini.
Alex Peters

Jawaban:

54

Ini secara eksplisit disebutkan di halaman doc Python (untuk Python 2.7 ) itu

Menggunakan iteritems()sambil menambah atau menghapus entri dalam kamus dapat menimbulkan RuntimeErroratau gagal mengulang semua entri.

Demikian pula untuk Python 3 .

Hal yang sama berlaku untuk iter(d), d.iterkeys()dan d.itervalues(), dan saya akan mengatakan bahwa itu berlaku untuk for k, v in d.items():(saya tidak ingat persis apa yang fordilakukannya, tetapi saya tidak akan terkejut jika penerapannya dipanggil iter(d)).

Raphaël Saint-Pierre
sumber
51
Saya akan mempermalukan diri saya sendiri demi komunitas dengan menyatakan bahwa saya menggunakan potongan kode itu sendiri. Berpikir bahwa karena saya tidak mendapatkan RuntimeError, saya pikir semuanya baik-baik saja. Dan itu terjadi, untuk sementara waktu. Unit tes retentif anal memberi saya acungan jempol dan bahkan berjalan dengan baik ketika dirilis. Kemudian, saya mulai berperilaku aneh. Apa yang terjadi adalah bahwa item dalam kamus dilewati sehingga tidak semua item dalam kamus dipindai. Anak-anak, belajarlah dari kesalahan yang telah saya buat dalam hidup saya dan katakan saja tidak! ;)
Alan Cabrera
3
Dapatkah saya mengalami masalah jika saya mengubah nilai pada kunci saat ini (tetapi tidak menambah atau menghapus kunci apa pun?) Saya akan membayangkan bahwa ini seharusnya tidak menimbulkan masalah, tetapi saya ingin tahu!
Gershy
@ GershomMaes Saya tidak tahu apa-apa, tetapi Anda mungkin masih berlari ke ladang ranjau jika tubuh lingkaran Anda menggunakan nilai dan tidak mengharapkannya untuk berubah.
Raphaël Saint-Pierre
3
d.items()harus aman dengan Python 2.7 (permainan berubah dengan Python 3), karena itu membuat apa yang pada dasarnya adalah salinan d, jadi Anda tidak memodifikasi apa yang Anda iterasi.
Harga Paul
Menarik untuk mengetahui apakah hal ini juga berlaku untukviewitems()
jlh
51

Alex Martelli mempertimbangkan ini di sini .

Mungkin tidak aman untuk mengganti wadah (mis. Dict) saat memutar ulang wadah. Jadi del d[f(k)]mungkin tidak aman. Seperti yang Anda ketahui, solusinya adalah dengan menggunakan d.items()(untuk mengulang salinan independen penampung), bukan d.iteritems()(yang menggunakan penampung pokok yang sama).

Tidak masalah untuk mengubah nilai pada indeks yang sudah ada dari dict, tetapi memasukkan nilai pada indeks baru (misalnya d[g(k)]=v) mungkin tidak berfungsi.

unutbu
sumber
3
Saya pikir ini adalah jawaban kunci bagi saya. Banyak kasus penggunaan akan memiliki satu proses memasukkan sesuatu dan proses lain membersihkan / menghapusnya sehingga saran untuk menggunakan d.items () berfungsi.
Peringatan
4
Informasi lebih lanjut tentang peringatan Python 3 dapat ditemukan di PEP 469 di mana padanan semantik dari metode dikt Python 2 yang disebutkan di atas disebutkan.
Lionel Brooks
1
"Tidak apa-apa untuk mengubah nilai pada indeks yang sudah ada" - apakah Anda memiliki referensi untuk ini?
Jonathon Reinhart
1
@JonathonReinhart: Tidak, saya tidak memiliki referensi untuk ini, tapi menurut saya ini cukup standar dengan Python. Misalnya, Alex Martelli adalah pengembang inti Python dan mendemonstrasikan penggunaannya di sini .
unutbu
27

Anda tidak dapat melakukan itu, setidaknya dengan d.iteritems(). Saya mencobanya, dan Python gagal

RuntimeError: dictionary changed size during iteration

Jika Anda malah menggunakan d.items(), maka itu berhasil.

Di Python 3, d.items()adalah tampilan ke dalam kamus, seperti d.iteritems()di Python 2. Untuk melakukan ini di Python 3, gunakan d.copy().items(). Ini juga akan memungkinkan kita untuk mengulang salinan kamus untuk menghindari modifikasi struktur data yang kita iterasi.

murgatroid99
sumber
2
Saya menambahkan Python 3 ke jawaban saya.
murgatroid99
2
Untuk diketahui, terjemahan literal (seperti yang digunakan oleh 2to3) dari Py2 d.items()ke Py3 adalah list(d.items()), meskipun d.copy().items()mungkin memiliki efisiensi yang sebanding.
Søren Løvborg
2
Jika objek dict sangat besar, apakah d.copy (). Items () efisien?
capung
12

Saya memiliki kamus besar yang berisi array Numpy, jadi hal dict.copy (). Keys () yang disarankan oleh @ murgatroid99 tidak layak (meskipun berhasil). Sebagai gantinya, saya baru saja mengonversi keys_view ke daftar dan itu berfungsi dengan baik (dengan Python 3.4):

for item in list(dict_d.keys()):
    temp = dict_d.pop(item)
    dict_d['some_key'] = 1  # Some value

Saya menyadari ini tidak menyelami dunia filosofis dari cara kerja bagian dalam Python seperti jawaban di atas, tetapi ini memberikan solusi praktis untuk masalah yang dinyatakan.

2cynykyl
sumber
6

Kode berikut menunjukkan bahwa ini tidak didefinisikan dengan baik:

def f(x):
    return x

def g(x):
    return x+1

def h(x):
    return x+10

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[g(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[h(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

Contoh pertama memanggil g (k), dan melempar pengecualian (kamus mengubah ukuran selama iterasi).

Contoh kedua memanggil h (k) dan tidak mengeluarkan pengecualian, tetapi menghasilkan:

{21: 'axx', 22: 'bxx', 23: 'cxx'}

Yang, melihat kodenya, tampaknya salah - saya akan mengharapkan sesuatu seperti:

{11: 'ax', 12: 'bx', 13: 'cx'}
combatdave
sumber
Saya dapat mengerti mengapa Anda mungkin berharap {11: 'ax', 12: 'bx', 13: 'cx'}tetapi 21,22,23 harus memberi Anda petunjuk tentang apa yang sebenarnya terjadi: loop Anda melewati item 1, 2, 3, 11, 12, 13 tetapi tidak berhasil mengambil yang kedua putaran item baru saat dimasukkan di depan item yang sudah Anda iterasi. Ubah h()untuk kembali x+5dan Anda mendapatkan x: 'axxx'dll. Atau 'x + 3' dan Anda mendapatkan yang luar biasa'axxxxx'
Duncan
Ya, kesalahan saya, saya khawatir - keluaran yang saya harapkan adalah {11: 'ax', 12: 'bx', 13: 'cx'}seperti yang Anda katakan, jadi saya akan memperbarui posting saya tentang itu. Bagaimanapun, ini jelas bukan perilaku yang didefinisikan dengan baik.
combatdave
1

Saya mendapat masalah yang sama dan saya menggunakan prosedur berikut untuk menyelesaikan masalah ini.

Daftar Python dapat diulang bahkan jika Anda memodifikasi selama iterasi di atasnya. jadi untuk kode berikut ini akan mencetak 1 tanpa batas.

for i in list:
   list.append(1)
   print 1

Jadi dengan menggunakan daftar dan diktekan secara kolaboratif, Anda dapat memecahkan masalah ini.

d_list=[]
 d_dict = {} 
 for k in d_list:
    if d_dict[k] is not -1:
       d_dict[f(k)] = -1 # rather than deleting it mark it with -1 or other value to specify that it will be not considered further(deleted)
       d_dict[g(k)] = v # add a new item 
       d_list.append(g(k))
Zeel Shah
sumber
Saya tidak yakin apakah aman untuk mengubah daftar selama iterasi (meskipun mungkin berhasil dalam beberapa kasus). Lihat pertanyaan ini misalnya ...
Roman
@Roman Jika Anda ingin menghapus elemen dari sebuah daftar, Anda dapat dengan aman mengulanginya dalam urutan terbalik, karena dalam urutan normal indeks dari elemen berikutnya akan berubah saat dihapus. Lihat contoh ini.
mbomb007
1

Python 3 Anda hanya harus:

prefix = 'item_'
t = {'f1': 'ffw', 'f2': 'fca'}
t2 = dict() 
for k,v in t.items():
    t2[k] = prefix + v

atau gunakan:

t2 = t1.copy()

Anda tidak boleh mengubah kamus asli, ini menyebabkan kebingungan serta potensi bug atau RunTimeErrors. Kecuali Anda hanya menambahkan ke kamus dengan nama kunci baru.

Dexter
sumber
0

Hari ini saya memiliki kasus penggunaan yang serupa, tetapi alih-alih hanya mewujudkan kunci di kamus di awal perulangan, saya ingin perubahan pada dict untuk memengaruhi iterasi dari dict, yang merupakan dict berurutan.

Saya akhirnya membangun rutinitas berikut, yang juga dapat ditemukan di jaraco.itertools :

def _mutable_iter(dict):
    """
    Iterate over items in the dict, yielding the first one, but allowing
    it to be mutated during the process.
    >>> d = dict(a=1)
    >>> it = _mutable_iter(d)
    >>> next(it)
    ('a', 1)
    >>> d
    {}
    >>> d.update(b=2)
    >>> list(it)
    [('b', 2)]
    """
    while dict:
        prev_key = next(iter(dict))
        yield prev_key, dict.pop(prev_key)

Docstring menggambarkan penggunaannya. Fungsi ini dapat digunakan sebagai pengganti di d.iteritems()atas untuk mendapatkan efek yang diinginkan.

Jason R. Coombs
sumber