반복하면서 세트에 추가하거나 제거 할 때 왜이 반복을 많이합니까?


61

파이썬 for-loop를 이해하려고 시도하면 {1}C 또는 다른 언어와 같이 반복을 수행하는지에 따라 한 번의 반복 결과 를 얻거나 무한 루프에 빠질 것이라고 생각 했습니다. 그러나 실제로는 그렇지 않았습니다.

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

왜 16 회 반복합니까? 결과는 {16}어디에서 오는가?

이것은 Python 3.8.2를 사용하고있었습니다. pypy에서는 예상 결과를 얻습니다 {1}.


17
추가 한 항목에 따라 각 호출 s.add(i+1)(및 가능하면 호출 s.remove(i))은 집합의 반복 순서를 변경하여 for 루프가 생성 한 집합 반복자가 다음에 보게되는 내용에 영향을 줄 수 있습니다. 반복자가 활성화되어있는 동안 객체를 변경하지 마십시오.
chepner

6
나는 또한 그것을 알아 차렸다. t = {16}그리고 t.add(15)t가 세트 {16, 15}라는 것을 산출한다. 문제가 어딘가에 있다고 생각합니다.

19
16은 15보다 낮은 해시 (@Anon이 알 수있는 것)를 가지고 있으므로 설정된 종류에 16을 추가하여 반복자의 "이미 볼 수있는"부분에 추가하여 반복자가 소진되었습니다.
Błotosmętek

1
trough de docs를 읽으면 루프 중에 반복자를 변경하면 버그가 발생할 수 있다는 메모가 있습니다. 참조 : docs.python.org/3.7/reference/...
마르첼로 파브리 지오

3
@ Błotosmętek : CPython 3.8.2에서 hash (16) == 16 및 hash (15) == 15.이 동작은 해시 자체가 더 낮기 때문에 발생하지 않습니다. 요소는 세트에서 해시 순서로 직접 저장되지 않습니다.
user2357112는

답변:


86

파이썬은이 루프가 언제 끝날지 약속하지 않습니다. 반복하는 동안 집합을 수정하면 요소를 건너 뛰고 요소를 반복하며 기타 이상한 점이 생길 수 있습니다. 그러한 행동에 의존하지 마십시오.

내가 말하려는 모든 것은 구현 세부 사항이며 예고없이 변경 될 수 있습니다. 그 중 하나에 의존하는 프로그램을 작성하면 프로그램은 CPython 3.8.2 이외의 Python 구현 및 버전의 조합에서 중단 될 수 있습니다.

루프가 16에서 끝나는 이유에 대한 간단한 설명은 16이 이전 요소보다 낮은 해시 테이블 인덱스에 배치되는 첫 번째 요소라는 것입니다. 전체 설명은 다음과 같습니다.


파이썬 세트의 내부 해시 테이블은 항상 2의 거듭 제곱을 갖습니다. 크기가 2 ^ n 인 테이블의 경우 충돌이 발생하지 않으면 해시의 n 개의 최하위 비트에 해당하는 해시 테이블의 위치에 요소가 저장됩니다. 다음에서 구현 된 것을 볼 수 있습니다 set_add_entry.

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

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

대부분의 작은 파이썬 정수는 스스로 해시합니다. 특히, 테스트의 모든 정수는 스스로 해시됩니다. 에서 구현 된 것을 볼 수 있습니다 long_hash. 세트에는 해시에서 같은 비트가 같은 두 개의 요소가 포함되지 않으므로 충돌이 발생하지 않습니다.


파이썬 세트 반복자는 세트의 내부 해시 테이블에 간단한 정수 인덱스를 가진 세트에서 위치를 추적합니다. 다음 요소가 요청되면 반복자는 해당 색인에서 시작하는 해시 테이블에서 채워진 항목을 검색 한 다음 저장된 색인을 찾은 항목 바로 다음으로 설정하고 항목의 요소를 리턴합니다. 당신은 이것을 볼 수 있습니다 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;

세트는 처음에 크기가 8 인 해시 테이블과 해시 테이블의 0인덱스 0에 있는 int 객체에 대한 포인터로 시작 합니다. 이터레이터는 인덱스 0에도 배치됩니다. 반복 할 때 요소가 해시 테이블에 추가됩니다. 각 인덱스는 다음 인덱스에 위치합니다. 왜냐하면 해시가 해당 요소를 배치하는 위치이므로 반복자가 보는 다음 인덱스입니다. 제거 된 요소에는 충돌 해결을 위해 더미 마커가 이전 위치에 저장되어 있습니다. 구현 된 것을 볼 수 있습니다 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;

4세트에 추가하면 세트의 요소 및 인형 수가 충분히 높아져 다음을 set_add_entry호출하여 해시 테이블 재 구축 을 트리거합니다 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해시 테이블에서 채워진 비 더미 항목의 수는 2이므로 set_table_resize두 번째 인수로 8을받습니다. 이를 기반으로 새 해시 테이블 크기는 16이되도록 set_table_resize 결정 합니다.

/* 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.
}

크기가 16 인 해시 테이블을 다시 작성합니다. 모든 요소는 여전히 해시에 높은 비트가 설정되어 있지 않기 때문에 새 해시 테이블의 이전 인덱스에서 끝납니다.

루프가 계속되면 요소는 반복자가 볼 다음 색인에 계속 배치됩니다. 다른 해시 테이블 재 구축이 트리거되지만 새 크기는 여전히 16입니다.

루프가 16을 요소로 추가하면 패턴이 끊어집니다. 새 요소를 배치 할 인덱스 16이 없습니다. 16의 4 가장 낮은 비트는 0000이며, 인덱스 0에 16을 배치합니다. 반복자의 저장된 색인은이 시점에서 16이며, 루프가 반복자에서 다음 요소를 요구할 때 반복자는 마지막 끝을지나 갔음을 알 수 있습니다. 해시 테이블.

반복자는이 시점에서 루프를 종료 16하고 세트 에만 남겨 둡니다 .


14

나는 이것이 파이썬에서 실제 세트 구현과 관련이 있다고 생각합니다. 세트는 항목을 저장하기 위해 해시 테이블을 사용하므로 세트를 반복하면 해시 테이블의 행을 반복한다는 의미입니다.

항목을 반복하여 세트에 추가하면 16 번에 도달 할 때까지 새 해시가 작성되어 해시 테이블에 추가됩니다.이 시점에서 다음 번호는 실제로 해시 테이블의 시작 부분에 추가되며 끝이 아닌 추가됩니다. 그리고 이미 테이블의 첫 번째 행을 반복했기 때문에 반복 루프가 끝납니다.

내 대답을 기반으로 비슷한 질문 중 하나, 실제로이 동일한 예를 보여줍니다. 더 자세한 내용을 읽어 보는 것이 좋습니다.


5

파이썬 3 문서에서 :

동일한 컬렉션을 반복하면서 컬렉션을 수정하는 코드는 까다로울 수 있습니다. 대신, 일반적으로 컬렉션의 복사본을 반복하거나 새 컬렉션을 만드는 것이 더 간단합니다.

사본을 반복

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

한 번만 반복해야합니다.

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

편집 :이 반복의 가능한 이유는 세트가 정렬되지 않아서 일종의 스택 추적 종류를 유발하기 때문입니다. 목록이 아닌 목록으로 목록을 작성하면 s = [1]목록이 정렬되므로 for 루프가 색인 0으로 시작한 다음 다음 색인으로 이동하여 목록이없는 것으로 확인합니다. 루프를 종료합니다.


예. 그러나 내 질문은 16 번 반복하는 이유입니다.
멍청한 오버플로

세트가 정렬되지 않았습니다. 사전 및 세트는 비 순서대로 반복되며,이 알고리즘은 사용자가 아무것도 수정하지 않은 경우에만 유지됩니다. 리스트와 튜플의 경우 인덱스별로 반복 할 수 있습니다. 3.7.2에서 코드를 시도하면 8 번 반복되었습니다.
Eric Jin

다른 사람들이 언급했듯이 반복 순서는 아마도 해싱과 관련이 있습니다.
Eric Jin

1
"일종의 스택 트레이스 정렬을 일으킨다"는 것은 무엇을 의미합니까? 코드에서 충돌이나 오류가 발생하지 않아 스택 추적이 표시되지 않았습니다. 파이썬에서 스택 추적을 어떻게 활성화합니까?
noob overflow

1

파이썬은 요소 위치 또는 삽입 순서를 기록하지 않는 비 순차 컬렉션을 설정했습니다. 파이썬 세트의 모든 요소에 연결된 인덱스가 없습니다. 따라서 인덱싱 또는 슬라이싱 작업을 지원하지 않습니다.

따라서 for 루프가 정의 된 순서대로 작동한다고 기대하지 마십시오.

왜 16 회 반복합니까?

user2357112 supports Monica이미 주요 원인을 설명합니다. 여기 또 다른 사고 방식이 있습니다.

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

이 코드를 실행하면 다음과 같이 출력됩니다.

{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}       

루프 또는 인쇄와 같이 모든 요소에 함께 액세스 할 때 전체 세트를 순회하려면 사전 정의 된 순서가 있어야합니다. 그래서, 마지막 반복에서 당신은 순서에서 같이 변경됩니다 볼 {i,i+1}{i+1,i}.

마지막 반복 후에 i+1이미 통과되었으므로 루프 종료.

재미있는 사실 : 6과 7이 항상 결과 16을 제공한다는 점을 제외하고 16보다 작은 값을 사용하십시오.


"16보다 작은 값을 사용하면 항상 16이됩니다." -6 또는 7로 시도하면 유지되지 않는 것을 볼 수 있습니다.
user2357112는 Monica

@ user2357112는 Monica를 지원합니다. 업데이트했습니다. 감사합니다
Eklavya
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.