Tại sao tôi nhận được nhiều lần lặp này khi thêm và xóa khỏi một tập hợp trong khi lặp qua nó?


62

Cố gắng hiểu vòng lặp Python, tôi nghĩ rằng điều này sẽ mang lại kết quả {1}cho một lần lặp hoặc chỉ bị mắc kẹt trong một vòng lặp vô hạn, tùy thuộc vào việc nó có lặp lại như trong C hoặc các ngôn ngữ khác hay không. Nhưng thật ra nó cũng không.

>>> s = {0}
>>> for i in s:
...     s.add(i + 1)
...     s.remove(i)
...
>>> print(s)
{16}

Tại sao nó làm 16 lần lặp? Kết quả {16}đến từ đâu?

Đây là sử dụng Python 3.8.2. Trên pypy nó làm cho kết quả mong đợi {1}.


17
Tùy thuộc vào các mục bạn thêm, mỗi lệnh gọi s.add(i+1)(và có thể, cuộc gọi đến s.remove(i)) có thể thay đổi thứ tự lặp của bộ, ảnh hưởng đến những gì bộ lặp lặp mà vòng lặp for tạo ra sẽ thấy tiếp theo. Đừng biến đổi một đối tượng trong khi bạn có một trình vòng lặp hoạt động.
chepner

6
Tôi cũng nhận thấy rằng t = {16}và sau đó t.add(15)mang lại rằng t là tập {16, 15}. Tôi nghĩ vấn đề là ở đâu đó.

19
Đây là một chi tiết triển khai - 16 có hàm băm thấp hơn 15 (đó là những gì @Anon nhận thấy), do đó, thêm 16 vào loại đã thêm nó vào phần "đã thấy" của trình lặp, và do đó, trình vòng lặp đã cạn kiệt.
Błotosmętek

1
Nếu bạn đọc máng de docs, có một lưu ý nói rằng việc lặp lại các vòng lặp trong vòng lặp có thể tạo ra một số lỗi. Xem: docs.python.org/3.7/reference/ khăn
Marcello Fabrizio

3
@ Błotosmętek: Trên CPython 3.8.2, hàm băm (16) == 16 và hàm băm (15) == 15. Hành vi không xuất phát từ chính hàm băm thấp hơn; các phần tử không được lưu trữ trực tiếp theo thứ tự băm trong một tập hợp.
user2357112 hỗ trợ Monica

Câu trả lời:


87

Python không hứa hẹn khi nào (nếu có) vòng lặp này sẽ kết thúc. Sửa đổi một tập hợp trong quá trình lặp có thể dẫn đến các yếu tố bị bỏ qua, các yếu tố lặp lại và sự kỳ lạ khác. Đừng bao giờ dựa vào hành vi như vậy.

Tất cả những gì tôi sắp nói là chi tiết thực hiện, có thể thay đổi mà không cần thông báo trước. Nếu bạn viết một chương trình dựa trên bất kỳ chương trình nào, chương trình của bạn có thể phá vỡ mọi sự kết hợp giữa triển khai Python và phiên bản khác với CPython 3.8.2.

Giải thích ngắn gọn về lý do tại sao vòng lặp kết thúc ở 16 là 16 là phần tử đầu tiên được đặt ở chỉ số bảng băm thấp hơn phần tử trước. Giải thích đầy đủ dưới đây.


Bảng băm bên trong của bộ Python luôn có sức mạnh 2 kích thước. Đối với bảng có kích thước 2 ^ n, nếu không có xung đột xảy ra, các phần tử được lưu trữ ở vị trí trong bảng băm tương ứng với n bit có ý nghĩa nhỏ nhất của hàm băm của chúng. Bạn có thể thấy điều này được thực hiện trong set_add_entry:

mask = so->mask;
i = (size_t)hash & mask;

entry = &so->table[i];
if (entry->key == NULL)
    goto found_unused;

Hầu hết các ints Python nhỏ băm vào chính họ; đặc biệt, tất cả các int trong băm thử nghiệm của bạn cho chính họ. Bạn có thể thấy điều này được thực hiện trong long_hash. Vì bộ của bạn không bao giờ chứa hai phần tử có bit thấp bằng nhau trong băm của chúng, không xảy ra xung đột.


Trình lặp Python set theo dõi vị trí của nó trong một tập hợp với chỉ số nguyên đơn giản vào bảng băm bên trong của tập hợp. Khi phần tử tiếp theo được yêu cầu, trình vòng lặp tìm kiếm mục nhập được điền vào bảng băm bắt đầu từ chỉ mục đó, sau đó đặt chỉ mục được lưu trữ của nó thành ngay sau mục nhập được tìm thấy và trả về phần tử của mục nhập. Bạn có thể thấy điều này trong setiter_iternext:

while (i <= mask && (entry[i].key == NULL || entry[i].key == dummy))
    i++;
si->si_pos = i+1;
if (i > mask)
    goto fail;
si->len--;
key = entry[i].key;
Py_INCREF(key);
return key;

Tập hợp ban đầu của bạn bắt đầu với bảng băm có kích thước 8 và một con trỏ tới một 0đối tượng int ở chỉ số 0 trong bảng băm. Trình lặp cũng được định vị ở chỉ số 0. Khi bạn lặp lại, các phần tử được thêm vào bảng băm, mỗi phần tử ở chỉ mục tiếp theo bởi vì đó là hàm băm của chúng nói để đặt chúng và đó luôn là chỉ mục tiếp theo mà trình lặp đó nhìn vào. Các phần tử bị loại bỏ có một điểm đánh dấu giả được lưu trữ tại vị trí cũ của chúng, cho mục đích giải quyết va chạm. Bạn có thể thấy điều đó được thực hiện trong set_discard_entry:

entry = set_lookkey(so, key, hash);
if (entry == NULL)
    return -1;
if (entry->key == NULL)
    return DISCARD_NOTFOUND;
old_key = entry->key;
entry->key = dummy;
entry->hash = -1;
so->used--;
Py_DECREF(old_key);
return DISCARD_FOUND;

Khi 4được thêm vào tập hợp, số phần tử và hình nộm trong tập hợp trở nên đủ cao để set_add_entrykích hoạt xây dựng bảng băm, gọi set_table_resize:

if ((size_t)so->fill*5 < mask*3)
    return 0;
return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4);

so->usedlà số lượng mục nhập giả, không giả trong bảng băm, là 2, vì vậy set_table_resizenhận được 8 là đối số thứ hai của nó. Dựa trên điều này, set_table_resize quyết định kích thước bảng băm mới phải là 16:

/* Find the smallest table size > minused. */
/* XXX speed-up with intrinsics */
size_t newsize = PySet_MINSIZE;
while (newsize <= (size_t)minused) {
    newsize <<= 1; // The largest possible value is PY_SSIZE_T_MAX + 1.
}

Nó xây dựng lại bảng băm với kích thước 16. Tất cả các phần tử vẫn kết thúc tại các chỉ mục cũ của chúng trong bảng băm mới, vì chúng không có bất kỳ bit cao nào được đặt trong băm.

Khi vòng lặp tiếp tục, các phần tử tiếp tục được đặt ở chỉ mục tiếp theo mà trình vòng lặp sẽ nhìn. Xây dựng lại bảng băm khác được kích hoạt, nhưng kích thước mới vẫn là 16.

Mẫu bị phá vỡ khi vòng lặp thêm 16 như là một phần tử. Không có chỉ số 16 để đặt phần tử mới tại. 4 bit thấp nhất của 16 là 0000, đặt 16 ở chỉ số 0. Chỉ số được lưu của iterator là 16 tại thời điểm này và khi vòng lặp yêu cầu phần tử tiếp theo từ iterator, iterator thấy rằng nó đã đi qua phần cuối của bảng băm.

Trình lặp kết thúc vòng lặp tại điểm này, chỉ để lại 16trong tập hợp.


14

Tôi tin rằng điều này có liên quan đến việc triển khai thực tế các bộ trong python. Các bộ sử dụng các bảng băm để lưu trữ các mục của chúng và do đó lặp qua một bộ có nghĩa là lặp qua các hàng của bảng băm của nó.

Khi bạn lặp lại và thêm các mục vào tập hợp của mình, các giá trị băm mới sẽ được tạo và gắn vào bảng băm cho đến khi bạn đạt đến số 16. Tại thời điểm này, số tiếp theo thực sự được thêm vào đầu bảng băm chứ không phải đến cuối. Và vì bạn đã lặp lại hàng đầu tiên của bảng, nên vòng lặp kết thúc.

Câu trả lời của tôi là dựa trên này một trong những câu hỏi tương tự, nó thực sự cho thấy ví dụ này cùng chính xác. Tôi thực sự khuyên bạn nên đọc nó để biết thêm chi tiết.


5

Từ tài liệu python 3:

Mã sửa đổi một bộ sưu tập trong khi lặp lại trên cùng một bộ sưu tập đó có thể khó khăn để có được quyền. Thay vào đó, thường là đơn giản hơn để lặp qua một bản sao của bộ sưu tập hoặc để tạo một bộ sưu tập mới:

Lặp lại một bản sao

s = {0}
s2 = s.copy()
for i in s2:
     s.add(i + 1)
     s.remove(i)

chỉ nên lặp lại 1 lần

>>> print(s)
{1}
>>> print(s2)
{0}

Chỉnh sửa: Một lý do có thể cho phép lặp này là do một tập hợp không có thứ tự, gây ra một số loại dấu vết ngăn xếp. Nếu bạn làm điều đó với một danh sách chứ không phải một tập hợp, thì nó sẽ kết thúc, s = [1]vì các danh sách được sắp xếp nên vòng lặp for sẽ bắt đầu với chỉ số 0 và sau đó chuyển sang chỉ mục tiếp theo, nhận thấy rằng không có một và thoát khỏi vòng lặp.


Đúng. Nhưng câu hỏi của tôi là tại sao nó thực hiện 16 lần lặp.
Noob tràn

thiết lập là không có thứ tự. Từ điển và thiết lập lặp theo thứ tự không ngẫu nhiên và thuật toán này lặp lại chỉ giữ nếu bạn không sửa đổi bất cứ điều gì. Đối với danh sách và bộ dữ liệu, nó chỉ có thể lặp theo chỉ mục. Khi tôi thử mã của bạn trong 3.7.2, nó đã thực hiện 8 lần lặp.
Eric Jin

Thứ tự lặp có lẽ phải làm với băm, như những người khác đã đề cập
Eric Jin

1
Điều đó có nghĩa là gì "gây ra một số loại sắp xếp theo dõi ngăn xếp"? Mã không gây ra sự cố hoặc lỗi vì vậy tôi không thấy bất kỳ dấu vết ngăn xếp nào. Làm cách nào để bật dấu vết ngăn xếp trong python?
Noob tràn

1

Python thiết lập một bộ sưu tập không có thứ tự không ghi vị trí phần tử hoặc thứ tự chèn. Không có chỉ mục nào được đính kèm với bất kỳ phần tử nào trong bộ python. Vì vậy, họ không hỗ trợ bất kỳ hoạt động lập chỉ mục hoặc cắt.

Vì vậy, đừng hy vọng vòng lặp for của bạn sẽ hoạt động theo thứ tự xác định.

Tại sao nó làm 16 lần lặp?

user2357112 supports Monicađã giải thích nguyên nhân chính. Đây là một cách nghĩ khác.

s = {0}
for i in s:
     s.add(i + 1)
     print(s)
     s.remove(i)
print(s)

Khi bạn chạy mã này, nó cung cấp cho bạn đầu ra này:

{0, 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}                                                                                                                             
{16, 15}                                                                                                                             
{16}       

Khi chúng ta truy cập tất cả các phần tử với nhau như vòng lặp hoặc in tập hợp, phải có một thứ tự được xác định trước để nó đi qua toàn bộ tập hợp. Vì vậy, trong lần lặp cuối cùng bạn sẽ thấy trật tự đang thay đổi như thế nào từ {i,i+1}đến {i+1,i}.

Sau lần lặp cuối cùng, nó đã xảy ra và i+1đã thoát qua vòng lặp.

Sự thật thú vị: Sử dụng bất kỳ giá trị nào nhỏ hơn 16 ngoại trừ 6 và 7 sẽ luôn mang lại cho bạn kết quả 16.


"Sử dụng bất kỳ giá trị nào dưới 16 sẽ luôn mang lại cho bạn kết quả 16." - hãy thử với 6 hoặc 7 và bạn sẽ thấy điều đó không giữ được.
user2357112 hỗ trợ Monica

@ user2357112 hỗ trợ Monica tôi đã cập nhật nó. Cảm ơn
Eklavya
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.