Sửa đổi một lệnh Python trong khi lặp lại nó


87

Giả sử chúng ta có một từ điển Python dvà chúng ta đang lặp lại nó như vậy:

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

( fgchỉ là một số phép biến đổi hộp đen.)

Nói cách khác, chúng tôi cố gắng thêm / xóa các mục dtrong khi lặp lại nó bằng cách sử dụng iteritems.

Điều này có được xác định rõ không? Bạn có thể cung cấp một số tài liệu tham khảo để hỗ trợ câu trả lời của bạn?

(Rõ ràng là làm thế nào để sửa lỗi này nếu nó bị hỏng, vì vậy đây không phải là góc tôi đang theo đuổi.)




Tôi đã cố gắng để làm điều này và có vẻ như rằng nếu bạn rời khỏi kích thước dict ban đầu không thay đổi - ví dụ như thay thế bất kỳ khóa / giá trị thay vì loại bỏ chúng sau đó mã này sẽ không ném ngoại lệ
Artsiom Rudzenka

Tôi không đồng ý rằng "khá rõ ràng cách sửa lỗi này nếu nó bị hỏng" đối với tất cả mọi người đang tìm kiếm chủ đề này (bao gồm cả bản thân tôi) và tôi ước câu trả lời được chấp nhận ít nhất đã chạm đến vấn đề này.
Alex Peters

Câu trả lời:


53

Nó được đề cập rõ ràng trên trang tài liệu Python (dành cho Python 2.7 ) rằng

Việc sử dụng iteritems()trong khi thêm hoặc xóa các mục nhập trong từ điển có thể làm tăng RuntimeErrorhoặc không lặp lại tất cả các mục nhập.

Tương tự đối với Python 3 .

Điều này cũng giữ cho iter(d), d.iterkeys()d.itervalues(), và tôi sẽ đi xa như nói rằng nó làm cho for k, v in d.items():(Tôi không thể nhớ chính xác những gì forkhông, nhưng tôi sẽ không ngạc nhiên nếu thực hiện gọi iter(d)).


48
Tôi sẽ xấu hổ vì lợi ích của cộng đồng khi tuyên bố rằng tôi đã sử dụng đoạn mã. Nghĩ rằng vì tôi không nhận được RuntimeError nên tôi nghĩ mọi thứ đều tốt. Và nó đã xảy ra trong một thời gian. Các bài kiểm tra đơn vị cẩn thận đã khiến tôi thích thú và nó thậm chí còn chạy tốt khi nó được phát hành. Sau đó, tôi bắt đầu có những hành vi kỳ lạ. Điều đã xảy ra là các mục trong từ điển bị bỏ qua và vì vậy không phải tất cả các mục trong từ điển đều được quét. Các con, hãy học từ những sai lầm mà mẹ đã mắc phải trong cuộc đời và chỉ cần nói không! ;)
Alan Cabrera

3
Tôi có thể gặp sự cố nếu tôi đang thay đổi giá trị ở khóa hiện tại (nhưng không thêm hoặc xóa bất kỳ khóa nào?) Tôi sẽ cho rằng điều này sẽ không gây ra bất kỳ sự cố nào, nhưng tôi muốn biết!
Gershom

@GershomMaes Tôi không biết bất kỳ điều gì, nhưng bạn có thể vẫn đang gặp phải một bãi mìn nếu phần thân vòng lặp của bạn sử dụng giá trị và không mong đợi nó thay đổi.
Raphaël Saint-Pierre

3
d.items()sẽ an toàn trong Python 2.7 (trò chơi thay đổi với Python 3), vì nó tạo ra những gì về cơ bản là một bản sao của nó d, vì vậy bạn sẽ không sửa đổi những gì bạn đang lặp lại.
Paul Price

Sẽ rất thú vị nếu biết điều này cũng đúng vớiviewitems()
jlh

50

Alex Martelli cân nhắc về điều này ở đây .

Có thể không an toàn khi thay đổi vùng chứa (ví dụ: dict) trong khi lặp qua vùng chứa. Vì vậy del d[f(k)]có thể không an toàn. Như bạn đã biết, cách giải quyết là sử dụng d.items()(lặp qua một bản sao độc lập của vùng chứa) thay vì d.iteritems()(sử dụng cùng một vùng chứa bên dưới).

Bạn có thể sửa đổi giá trị tại chỉ mục hiện có của dict, nhưng việc chèn giá trị tại các chỉ mục mới (ví dụ d[g(k)]=v) có thể không hoạt động.


3
Tôi nghĩ rằng đây là một câu trả lời quan trọng cho tôi. Rất nhiều trường hợp sử dụng sẽ có một quy trình chèn mọi thứ và một quy trình khác dọn dẹp / xóa chúng, vì vậy lời khuyên sử dụng d.items () hoạt động. Python 3 hãy cẩn thận không chịu được
easytiger

4
Có thể tìm thấy thêm thông tin về các cảnh báo Python 3 trong PEP 469 , nơi các phương thức tương đương về ngữ nghĩa của các phương thức dict Python 2 nói trên được liệt kê.
Lionel Brooks

1
"Có thể sửa đổi giá trị tại một chỉ mục hiện có của dict" - bạn có tài liệu tham khảo cho điều này không?
Jonathon Reinhart

1
@JonathonReinhart: Không, tôi không có tài liệu tham khảo cho điều này, nhưng tôi nghĩ nó khá chuẩn trong Python. Ví dụ, Alex Martelli là một nhà phát triển cốt lõi của Python và trình bày cách sử dụng nó ở đây .
unutbu

27

Bạn không thể làm điều đó, ít nhất là với d.iteritems(). Tôi đã thử và Python không thành công với

RuntimeError: dictionary changed size during iteration

Nếu bạn sử dụng thay vào đó d.items(), thì nó hoạt động.

Trong Python 3, d.items()là một dạng xem trong từ điển, giống như d.iteritems()trong Python 2. Để thực hiện điều này trong Python 3, thay vào đó hãy sử dụng d.copy().items(). Tương tự, điều này sẽ cho phép chúng tôi lặp qua một bản sao của từ điển để tránh sửa đổi cấu trúc dữ liệu mà chúng tôi đang lặp lại.


2
Tôi đã thêm Python 3 vào câu trả lời của mình.
Murgatroid99

2
FYI, bản dịch theo nghĩa đen (ví dụ được sử dụng bởi 2to3) từ Py2's d.items()sang Py3 list(d.items()), mặc dù d.copy().items()có lẽ có hiệu quả tương đương.
Søren Løvborg

2
Nếu đối tượng dict rất lớn, liệu d.copy (). Items () có hiệu quả không?
chuồn chuồn

11

Tôi có một từ điển lớn chứa các mảng Numpy, vì vậy điều dict.copy (). Key () được đề xuất bởi @ murgatroid99 là không khả thi (mặc dù nó đã hoạt động). Thay vào đó, tôi chỉ chuyển đổi key_view thành một danh sách và nó hoạt động tốt (trong Python 3.4):

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

Tôi nhận thấy rằng điều này không đi sâu vào lĩnh vực triết học của hoạt động bên trong Python như các câu trả lời ở trên, nhưng nó cung cấp một giải pháp thực tế cho vấn đề đã nêu.


6

Đoạn mã sau cho thấy điều này không được xác định rõ:

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

Ví dụ đầu tiên gọi g (k) và ném một ngoại lệ (từ điển đã thay đổi kích thước trong quá trình lặp).

Ví dụ thứ hai gọi h (k) và không ném ngoại lệ, nhưng kết quả đầu ra:

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

Mà, nhìn vào mã, có vẻ sai - tôi đã mong đợi một cái gì đó như:

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

Tôi có thể hiểu tại sao bạn có thể mong đợi {11: 'ax', 12: 'bx', 13: 'cx'}nhưng 21,22,23 sẽ cung cấp cho bạn manh mối về những gì thực sự đã xảy ra: vòng lặp của bạn đã đi qua các mục 1, 2, 3, 11, 12, 13 nhưng không quản lý để chọn mục thứ hai vòng các mục mới khi chúng được chèn vào trước các mục bạn đã lặp lại. Thay đổi h()để trở lại x+5và bạn sẽ có được một x: 'axxx'vv hoặc 'x + 3' và bạn sẽ có được lộng lẫy'axxxxx'
Duncan

Vâng, sai lầm của tôi, tôi sợ - đầu ra mong đợi của tôi {11: 'ax', 12: 'bx', 13: 'cx'}như bạn đã nói, vì vậy tôi sẽ cập nhật bài đăng của mình về nó. Dù bằng cách nào, đây rõ ràng không phải là hành vi được xác định rõ ràng.
battledave,

1

Tôi gặp vấn đề tương tự và tôi đã sử dụng quy trình sau để giải quyết vấn đề này.

Danh sách Python có thể được lặp lại ngay cả khi bạn sửa đổi trong khi lặp qua nó. vì vậy đối với mã sau, nó sẽ in vô hạn số 1.

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

Vì vậy, sử dụng danh sách và chính tả một cách cộng tác, bạn có thể giải quyết vấn đề này.

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

Tôi không chắc liệu việc sửa đổi danh sách trong quá trình lặp lại có an toàn hay không (mặc dù nó có thể hoạt động trong một số trường hợp). Hãy xem câu hỏi này chẳng hạn ...
Roman

@Roman Nếu bạn muốn xóa các phần tử của danh sách, bạn có thể lặp lại nó một cách an toàn theo thứ tự ngược lại, vì theo thứ tự bình thường, chỉ mục của phần tử tiếp theo sẽ thay đổi khi xóa. Hãy xem ví dụ này.
mbomb007

1

Python 3 bạn chỉ nên:

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

hoặc dùng:

t2 = t1.copy()

Bạn không bao giờ nên sửa đổi từ điển gốc, nó dẫn đến nhầm lẫn cũng như các lỗi tiềm ẩn hoặc RunTimeErrors. Trừ khi bạn chỉ thêm vào từ điển các tên khóa mới.


0

Hôm nay tôi gặp một trường hợp sử dụng tương tự, nhưng thay vì chỉ đơn giản hóa các khóa trên từ điển ở đầu vòng lặp, tôi muốn các thay đổi đối với dict ảnh hưởng đến việc lặp lại dict, đó là một dict có thứ tự.

Tôi đã kết thúc việc xây dựng quy trình sau, cũng có thể được tìm thấy trong 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)

Chuỗi doc minh họa cách sử dụng. Chức năng này có thể được sử dụng thay cho chức năng d.iteritems()trên để có hiệu quả mong muốn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.