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_entry
kí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->used
là số lượng mục nhập giả, không giả trong bảng băm, là 2, vì vậy set_table_resize
nhậ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 16
trong tập hợp.
s.add(i+1)
(và có thể, cuộc gọi đếns.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.