การแก้ไข Python dict ในขณะที่ทำซ้ำ


87

สมมติว่าเรามีพจนานุกรม Python dและเรากำลังทำซ้ำดังนี้:

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

( fและgเป็นเพียงการเปลี่ยนแปลงของกล่องดำ)

ในคำอื่น ๆ เราพยายามที่จะเพิ่ม / ลบรายการไปในขณะที่การทำซ้ำมากกว่านั้นโดยใช้diteritems

มีการกำหนดไว้อย่างดีหรือไม่? คุณช่วยให้ข้อมูลอ้างอิงเพื่อสนับสนุนคำตอบของคุณได้ไหม

(ค่อนข้างชัดเจนว่าจะแก้ไขได้อย่างไรถ้ามันเสียนี่ไม่ใช่มุมที่ฉันตามมา)




ฉันได้ลองทำสิ่งนี้แล้วและดูเหมือนว่าหากคุณปล่อยให้ขนาดของคำสั่งเริ่มต้นไม่เปลี่ยนแปลง - เช่นแทนที่คีย์ / ค่าใด ๆ แทนที่จะลบออกรหัสนี้จะไม่ส่งข้อยกเว้น
Artsiom Rudzenka

ฉันไม่เห็นด้วยที่“ ค่อนข้างชัดเจนว่าจะแก้ไขปัญหานี้ได้อย่างไรถ้ามันเสีย” สำหรับทุกคนที่ค้นหาหัวข้อนี้ (รวมถึงตัวฉันเอง) และฉันหวังว่าอย่างน้อยคำตอบที่ยอมรับจะได้สัมผัสกับสิ่งนี้
Alex Peters

คำตอบ:


54

มีการกล่าวถึงอย่างชัดเจนในหน้า Python doc (สำหรับPython 2.7 ) ว่า

การใช้iteritems()ในขณะที่เพิ่มหรือลบรายการในพจนานุกรมอาจเพิ่มRuntimeErrorหรือล้มเหลวในการวนซ้ำในทุกรายการ

ในทำนองเดียวกันสำหรับหลาม 3

เดียวกันถือสำหรับiter(d), d.iterkeys()และd.itervalues()และฉันจะไปเท่าที่บอกว่ามันไม่สำหรับfor k, v in d.items():(ผมจำไม่ได้ว่าสิ่งที่forไม่ แต่ฉันจะไม่ต้องแปลกใจถ้าการดำเนินการที่เรียกว่าiter(d))


51
ฉันจะอับอายตัวเองเพื่อประโยชน์ของชุมชนโดยระบุว่าฉันใช้ข้อมูลโค้ดมาก คิดตั้งแต่ยังไม่ได้ใช้ RuntimeError ก็คิดว่าทุกอย่างดีหมด และในขณะที่ การทดสอบหน่วยที่เก็บรักษาไว้ทำให้ฉันยกนิ้วให้และมันก็ยังทำงานได้ดีเมื่อเปิดตัว จากนั้นฉันก็เริ่มมีพฤติกรรมแปลก ๆ สิ่งที่เกิดขึ้นคือรายการในพจนานุกรมถูกข้ามไปและไม่ได้มีการสแกนรายการทั้งหมดในพจนานุกรม เด็ก ๆ เรียนรู้จากข้อผิดพลาดที่เกิดขึ้นในชีวิตและบอกว่าไม่! ;)
Alan Cabrera

3
ฉันจะพบปัญหาได้หรือไม่หากฉันเปลี่ยนค่าที่คีย์ปัจจุบัน (แต่ไม่ได้เพิ่มหรือลบคีย์ใด ๆ ) ฉันจะบอกว่าสิ่งนี้ไม่ควรทำให้เกิดปัญหาใด ๆ แต่ฉันอยากรู้!
Gershy

@GershomMaes ฉันไม่รู้อะไรเลย แต่คุณอาจยังวิ่งเข้าไปในเขตที่วางทุ่นระเบิดได้หากร่างกายห่วงของคุณใช้ประโยชน์จากคุณค่าและไม่คาดหวังว่ามันจะเปลี่ยนแปลง
Raphaël Saint-Pierre

3
d.items()ควรปลอดภัยใน Python 2.7 (เกมเปลี่ยนเป็น Python 3) เนื่องจากมันสร้างสิ่งที่เป็นสำเนาdดังนั้นคุณจึงไม่ได้แก้ไขสิ่งที่คุณกำลังทำซ้ำ
ราคา Paul

มันน่าสนใจที่จะรู้ว่านี่เป็นเรื่องจริงหรือไม่viewitems()
jlh

51

อเล็กซ์เทน้ำหนักในเกี่ยวกับเรื่องนี้ที่นี่

อาจไม่ปลอดภัยที่จะเปลี่ยนคอนเทนเนอร์ (เช่น dict) ในขณะที่วนรอบคอนเทนเนอร์ ดังนั้นdel d[f(k)]อาจไม่ปลอดภัย ดังที่คุณทราบวิธีแก้ปัญหาคือการใช้d.items()(เพื่อวนซ้ำบนสำเนาอิสระของคอนเทนเนอร์) แทนd.iteritems()(ซึ่งใช้คอนเทนเนอร์พื้นฐานเดียวกัน)

สามารถแก้ไขค่าที่ดัชนีที่มีอยู่ของ dict ได้ แต่การแทรกค่าในดัชนีใหม่ (เช่นd[g(k)]=v) อาจไม่ได้ผล


3
ฉันคิดว่านี่เป็นคำตอบที่สำคัญสำหรับฉัน กรณีการใช้งานจำนวนมากจะมีกระบวนการหนึ่งในการแทรกสิ่งต่าง ๆ และอีกกระบวนการหนึ่งทำความสะอาด / ลบสิ่งเหล่านี้ดังนั้นคำแนะนำในการใช้ d.items () จึงใช้ได้ผล คำเตือน Python 3 ไม่ทน
easytiger

4
ข้อมูลเพิ่มเติมเกี่ยวกับคำเตือน Python 3 สามารถพบได้ในPEP 469ซึ่งมีการแจกแจงความหมายที่เทียบเท่ากับวิธีการเขียนตามคำสั่ง Python 2 ดังกล่าว
Lionel Brooks

1
"สามารถแก้ไขค่าที่ดัชนีที่มีอยู่ของ dict" - คุณมีข้อมูลอ้างอิงสำหรับสิ่งนี้หรือไม่?
Jonathon Reinhart

1
@JonathonReinhart: ไม่ฉันไม่มีข้อมูลอ้างอิงสำหรับสิ่งนี้ แต่ฉันคิดว่ามันค่อนข้างเป็นมาตรฐานใน Python ยกตัวอย่างเช่นอเล็กซ์เทลเป็นนักพัฒนาหลักงูหลามและแสดงให้เห็นถึงการใช้งานที่นี่
unutbu

27

d.iteritems()คุณไม่สามารถทำอย่างนั้นอย่างน้อยด้วย ฉันลองแล้ว Python ล้มเหลวด้วย

RuntimeError: dictionary changed size during iteration

ถ้าคุณใช้ d.items()ว่าได้ผล

ใน Python 3 d.items()เป็นมุมมองในพจนานุกรมเช่นเดียวกับd.iteritems()ใน Python 2 หากต้องการทำสิ่งนี้ใน Python 3 ให้ใช้d.copy().items(). วิธีนี้จะช่วยให้เราสามารถทำสำเนาพจนานุกรมซ้ำได้ในทำนองเดียวกันเพื่อหลีกเลี่ยงการแก้ไขโครงสร้างข้อมูลที่เรากำลังทำซ้ำ


2
ฉันเพิ่ม Python 3 ในคำตอบของฉัน
murgatroid99

2
FYI การแปลตามตัวอักษร (เช่นใช้โดย2to3) ของ Py2 เป็นd.items()Py3 list(d.items())แม้ว่าd.copy().items()อาจจะมีประสิทธิภาพเทียบเท่ากัน
SørenLøvborg

2
ถ้าวัตถุ dict มีขนาดใหญ่มาก d.copy (). items () มีประสิทธิภาพหรือไม่?
แมลงปอ

12

ฉันมีพจนานุกรมขนาดใหญ่ที่มีอาร์เรย์ Numpy ดังนั้นสิ่งที่ dict.copy (). keys () แนะนำโดย @ murgatroid99 จึงไม่เป็นไปได้ (แม้ว่าจะใช้งานได้) แต่ฉันเพิ่งแปลง keys_view เป็นรายการและใช้งานได้ดี (ใน Python 3.4):

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

ฉันตระหนักดีว่าสิ่งนี้ไม่ได้ดำดิ่งลงไปในขอบเขตทางปรัชญาของการทำงานภายในของ Python เหมือนคำตอบข้างต้น แต่มันให้วิธีแก้ปัญหาที่เป็นประโยชน์


6

รหัสต่อไปนี้แสดงให้เห็นว่าสิ่งนี้ไม่ได้กำหนดไว้อย่างชัดเจน:

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

ตัวอย่างแรกเรียก g (k) และแสดงข้อยกเว้น (พจนานุกรมเปลี่ยนขนาดระหว่างการวนซ้ำ)

ตัวอย่างที่สองเรียก h (k) และพ่นไม่มีข้อยกเว้น แต่เอาต์พุต:

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

ซึ่งเมื่อดูรหัสแล้วดูเหมือนจะผิด - ฉันคาดหวังไว้ว่า:

{11: 'ax', 12: 'bx', 13: 'cx'}

ฉันเข้าใจว่าทำไมคุณถึงคาดหวัง{11: 'ax', 12: 'bx', 13: 'cx'}แต่ 21,22,23 ควรให้เบาะแสว่าเกิดอะไรขึ้นจริง: ลูปของคุณผ่านรายการที่ 1, 2, 3, 11, 12, 13 แต่ไม่สามารถรับสิ่งที่สองได้ รอบของรายการใหม่เมื่อพวกเขาถูกแทรกไว้ด้านหน้าของรายการที่คุณได้ทำซ้ำไปแล้ว เปลี่ยนh()เป็นผลตอบแทนx+5และคุณจะได้รับ x: 'axxx'ฯลฯ หรือ 'x + 3' อีกอันและคุณจะได้รับความงดงาม'axxxxx'
ดันแคน

ใช่ความผิดพลาดของฉันฉันกลัว - ผลลัพธ์ที่คาดหวังของฉันเป็นไป{11: 'ax', 12: 'bx', 13: 'cx'}ตามที่คุณพูดดังนั้นฉันจะอัปเดตโพสต์ของฉันเกี่ยวกับเรื่องนี้ ไม่ว่าจะด้วยวิธีใดพฤติกรรมนี้ไม่ได้กำหนดไว้อย่างชัดเจน
combatdave

1

ฉันพบปัญหาเดียวกันและฉันใช้ขั้นตอนต่อไปนี้เพื่อแก้ไขปัญหานี้

Python List สามารถทำซ้ำได้แม้ว่าคุณจะแก้ไขระหว่างการทำซ้ำ ดังนั้นสำหรับรหัสต่อไปนี้มันจะพิมพ์ 1 ไปเรื่อย ๆ

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

ดังนั้นการใช้รายการและเขียนตามคำบอกร่วมกันคุณสามารถแก้ปัญหานี้ได้

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

ฉันไม่แน่ใจว่าการแก้ไขรายการระหว่างการทำซ้ำจะปลอดภัยหรือไม่ (แม้ว่าอาจได้ผลในบางกรณี) ดูคำถามนี้เช่น ...
โรมัน

@ โรมันหากคุณต้องการลบองค์ประกอบของรายการคุณสามารถทำซ้ำได้อย่างปลอดภัยในลำดับย้อนกลับเนื่องจากในลำดับปกติดัชนีขององค์ประกอบถัดไปจะเปลี่ยนไปเมื่อลบ ดูตัวอย่างนี้
mbomb007

1

Python 3 คุณควร:

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

หรือใช้:

t2 = t1.copy()

คุณไม่ควรแก้ไขพจนานุกรมต้นฉบับเพราะจะนำไปสู่ความสับสนรวมทั้งข้อบกพร่องที่อาจเกิดขึ้นหรือ RunTimeErrors เว้นแต่คุณจะต่อท้ายพจนานุกรมด้วยชื่อคีย์ใหม่


0

วันนี้ฉันมีกรณีการใช้งานที่คล้ายกัน แต่แทนที่จะเป็นเพียงแค่การแสดงคีย์ในพจนานุกรมที่จุดเริ่มต้นของลูปฉันต้องการให้การเปลี่ยนแปลงของคำสั่งมีผลต่อการวนซ้ำของคำสั่งซึ่งเป็นคำสั่งตามคำสั่ง

ฉันลงเอยด้วยการสร้างกิจวัตรต่อไปนี้ซึ่งสามารถพบได้ใน 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 แสดงการใช้งาน ฟังก์ชันนี้สามารถใช้แทนd.iteritems()ด้านบนเพื่อให้ได้เอฟเฟกต์ที่ต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.