[python] 사전에서 순서가 임의 인 이유는 무엇입니까?

사전을 반복하거나 파이썬으로 설정하는 것이 ‘임의’순서에 의해 어떻게 수행되는지 이해하지 못합니다.

내 말은, 그것은 프로그래밍 언어이므로 언어의 모든 것이 100 % 결정되어야합니까? 파이썬에는 사전이나 세트의 어느 부분이 1, 2 등으로 선택되는지를 결정하는 일종의 알고리즘이 있어야합니다.

내가 무엇을 놓치고 있습니까?



답변

참고 : 이 답변은 dictPython 3.6에서 유형 의 구현이 변경 되기 전에 작성 되었습니다. 이 답변의 대부분의 구현 세부 사항은 여전히 ​​적용되지만 사전 의 키 목록 순서 는 더 이상 해시 값으로 결정되지 않습니다. 설정된 구현은 변경되지 않습니다.

순서는 임의적이지 않지만 사전 또는 세트의 삽입 및 삭제 기록과 특정 Python 구현에 따라 다릅니다. 이 답변의 나머지 부분에서 ‘사전’의 경우 ‘set’을 읽을 수도 있습니다. 집합은 키만 있고 값이없는 사전으로 구현됩니다.

키가 해시되고 해시 값이 동적 테이블의 슬롯에 할당됩니다 (필요에 따라 증가 또는 축소 될 수 있음). 그리고 매핑 프로세스는 충돌을 일으킬 수 있습니다. 즉, 이미 존재하는 것을 기반으로 다음 슬롯 에 키를 슬롯해야합니다 .

내용을 나열하면 슬롯에 걸쳐 반복되므로 키가 현재 테이블 에있는 순서대로 나열됩니다 .

열쇠를 가지고 'foo''bar', 예를 들어, 테이블의 크기는 8 개 슬롯 가정 할 수 있습니다. 파이썬 2.7 년 hash('foo')이다 -4177197833195190597, hash('bar')입니다 327024216814240868. 모듈로 8은이 두 키가 슬롯 3과 4에 슬롯 화되어 있음을 의미합니다.

>>> hash('foo')
-4177197833195190597
>>> hash('foo') % 8
3
>>> hash('bar')
327024216814240868
>>> hash('bar') % 8
4

목록 순서를 알려줍니다.

>>> {'bar': None, 'foo': None}
{'foo': None, 'bar': None}

3과 4를 제외한 모든 슬롯은 비어 있으며 먼저 테이블을 반복하여 슬롯 3을 나열한 다음 슬롯 4 'foo'를 나열 하므로 앞에 나열됩니다 'bar'.

bar그리고 baz, 그러나, 동일한 슬롯에 매핑 떨어져있어 정확히 8있는 해시 값을 가지고 4:

>>> hash('bar')
327024216814240868
>>> hash('baz')
327024216814240876
>>> hash('bar') % 8
4
>>> hash('baz') % 8
4

순서는 이제 어떤 키가 먼저 슬롯에 달려 있는지에 따라 다릅니다. 두 번째 키는 다음 슬롯으로 이동해야합니다.

>>> {'baz': None, 'bar': None}
{'bar': None, 'baz': None}
>>> {'bar': None, 'baz': None}
{'baz': None, 'bar': None}

하나 또는 다른 키가 먼저 슬롯되었으므로 테이블 순서는 여기에서 다릅니다.

CPython (가장 일반적으로 사용되는 Python 구현)에서 사용하는 기본 구조의 기술적 이름 은 개방 주소 지정을 사용 하는 해시 테이블 입니다. 호기심이 많고 C를 충분히 이해한다면 모든 (잘 문서화 된) 세부 사항 에 대한 C 구현 을 살펴보십시오 . CPython의 작동 방식 에 대한 Brandon Rhodes의 Pycon 2010 프레젠테이션을 보거나 Andrew Kuchling 이 작성한 구현에 대한 장을 포함하는 Beautiful Codedict 사본을 선택할 수도 있습니다.

Python 3.3부터는 임의 해시 시드도 사용되므로 특정 유형의 서비스 거부 (공격자가 대량 해시 충돌을 일으켜 Python 서버가 응답하지 않는 경우)를 방지하기 위해 해시 충돌을 예측할 수 없습니다. 주어진 또는 사전 설정된 순서가 다음 인 것이 수단이 또한 현재 파이썬 호출을위한 임의의 해시 시드에 의존.

다른 구현은 문서화 된 Python 인터페이스를 만족하는 한 사전에 다른 구조를 자유롭게 사용할 수 있지만 지금까지 모든 구현에서 해시 테이블의 변형을 사용한다고 생각합니다.

CPython 3.6은 삽입 순서를 유지하고 부팅하는 데 더 빠르고 더 메모리 효율적인 새로운 dict 구현을 도입했습니다 . 새로운 행은 각 행이 저장된 해시 값과 키 및 값 개체를 참조하는 큰 희소 테이블을 유지하기보다는 별도의 ‘밀도’테이블 (인덱스 수만 포함)의 인덱스 만 참조 하는 작은 해시 배열 을 추가합니다. 실제 키-값 쌍이 있기 때문에 포함 된 항목을 순서대로 나열하는 조밀 한 테이블입니다. 자세한 내용은 Python-Dev 제안을 참조하십시오 . 파이썬 3.6에서 이것은 구현 세부 사항으로 간주됩니다.Python-the-language는 다른 구현이 순서를 유지해야한다고 지정하지 않습니다. 이것은 파이썬 3.7에서 변경되었는데,이 세부 사항은 언어 사양 으로 향상되었습니다 . 모든 구현이 Python 3.7 이상과 올바르게 호환 되려면 이 순서 유지 동작을 복사 해야합니다 . 그리고 명확하게 말하면 :이 변경 사항은 세트에 이미 ‘작은’해시 구조가 있으므로 세트에는 적용되지 않습니다.

Python 2.7 이상은 키 순서를 기록하기 위해 추가 데이터 구조를 추가하는 하위 클래스 인 OrderedDictclass를 제공합니다 dict. 약간의 속도와 여분의 메모리 가격으로이 클래스는 키를 삽입 한 순서를 기억합니다. 키, 값 또는 항목을 나열하면 순서대로 나열됩니다. 주문을 효율적으로 최신 상태로 유지하기 위해 추가 사전에 저장된 이중 연결 목록을 사용합니다. Raymond Hettinger글을 참조하여 아이디어를 요약하십시오 . OrderedDict객체는 재정렬 과 같은 다른 장점이 있습니다 .

주문한 세트를 원하면 oset패키지를 설치할 수 있습니다 . 파이썬 2.5 이상에서 작동합니다.


답변

이것은 파이썬 3.41 A에 대한 응답 입니다.


다른 사람들도 옳습니다. 명령에 의존하지 마십시오. 존재하는 척조차하지 마십시오.

즉, 신뢰할 수있는 가지가 있습니다.

list(myset) == list(myset)

즉, 순서가 안정적 입니다.


지각 된 순서 가있는 이유를 이해 하려면 몇 가지 사항을 이해해야합니다.

  • 파이썬은 해시 세트를 사용 합니다 .

  • CPython의 해시 세트가 메모리에 저장되는 방법

  • 숫자가 해시되는 방법

상단에서 :

해시 세트 정말 빠른 검색 시간으로 임의 데이터를 저장하는 방법이다.

백업 배열이 있습니다.

# A C array; items may be NULL,
# a pointer to an object, or a
# special dummy object
_ _ 4 _ _ 2 _ _ 6

우리는 특수 더미 객체를 무시할 것입니다.이 더미는 제거하기가 쉽지 않기 때문에 제거하기가 더 쉽습니다.

정말 빠른 조회를 위해 객체에서 해시를 계산하는 마술을합니다. 유일한 규칙은 동일한 두 객체가 동일한 해시를 갖는 것입니다. 그러나 두 객체에 동일한 해시가 있으면 동일하지 않을 수 있습니다.

그런 다음 배열 길이로 모듈러스를 가져와 인덱스를 만듭니다.

hash(4) % len(storage) = index 2

따라서 요소에 빠르게 액세스 할 수 있습니다.

해시는 다음과 같이 이야기의 대부분을하다 hash(n) % len(storage)hash(m) % len(storage)같은 숫자가 발생할 수 있습니다. 이 경우 여러 가지 다른 전략이 충돌을 시도하고 해결할 수 있습니다. CPython은 복잡한 작업을 수행하기 전에 “선형 프로빙”을 9 번 사용하므로 다른 곳을보기 전에 슬롯 왼쪽 에서 최대 9 개의 위치를 ​​찾습니다.

CPython의 해시 세트는 다음과 같이 저장됩니다.

  • 해시 세트는 2/3 이하일없습니다 . 요소가 20 개이고 백업 배열의 길이가 30 개이면 백업 저장소의 크기가 더 커집니다. 작은 백업 저장소와의 충돌이 자주 발생하고 충돌로 인해 모든 속도가 느려지기 때문입니다.

  • 백업 저장소는 2의 거듭 제곱으로 크기가 조정되는 큰 세트 (50k 요소)를 제외하고 8에서 시작하여 4의 거듭 제곱으로 크기가 조정됩니다 (8, 32, 128, …).

따라서 배열을 만들 때 백업 저장소의 길이는 8입니다. 5가 가득 차서 요소를 추가하면 간단히 6 개의 요소가 포함됩니다. 6 > ²⁄₃·8이렇게하면 크기 조정이 시작되고 백업 저장소는 크기가 4 배가됩니다.

마지막으로 숫자를 hash(n)반환 n합니다 ( -1특별한 제외 ).


첫 번째를 보자.

v_set = {88,11,1,33,21,3,7,55,37,8}

len(v_set)은 10이므로 모든 항목이 추가 된 후 백업 저장소는 15 (+1) 이상 입니다. 2의 관련 거듭 제곱은 32입니다. 따라서 백업 저장소는 다음과 같습니다.

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

우리는

hash(88) % 32 = 24
hash(11) % 32 = 11
hash(1)  % 32 = 1
hash(33) % 32 = 1
hash(21) % 32 = 21
hash(3)  % 32 = 3
hash(7)  % 32 = 7
hash(55) % 32 = 23
hash(37) % 32 = 5
hash(8)  % 32 = 8

따라서 다음과 같이 삽입하십시오.

__  1 __  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __
   33 ← Can't also be where 1 is;
        either 1 or 33 has to move

그래서 우리는 같은 순서를 기대할 것입니다

{[1 or 33], 3, 37, 7, 8, 11, 21, 55, 88}

다른 곳에서 시작하지 않는 1 또는 33으로. 이것은 선형 프로빙을 사용하므로 다음 중 하나를 갖습니다.

       ↓
__  1 33  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

또는

       ↓
__ 33  1  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

1이 이미 있었기 때문에 33이 변위 된 것으로 예상 할 수 있지만 세트가 빌드 될 때 발생하는 크기 조정으로 인해 실제로는 그렇지 않습니다. 세트가 재구성 될 때마다 이미 추가 된 항목이 효과적으로 재정렬됩니다.

지금 당신은 이유를 볼 수 있습니다

{7,5,11,1,4,13,55,12,2,3,6,20,9,10}

순서대로있을 수 있습니다. 14 개의 요소가 있으므로 백업 저장소는 21 + 1 이상이며 이는 32를 의미합니다.

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

처음 13 개 슬롯에서 1 ~ 13 개의 해시. 20은 슬롯 20에 들어갑니다.

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ __ __ __ __ __ __ __ __ __

55는 슬롯 hash(55) % 3223에 들어갑니다 .

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ 55 __ __ __ __ __ __ __ __

대신 50을 선택하면

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ 50 __ 20 __ __ __ __ __ __ __ __ __ __ __

그리고 보라.

{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 20, 50}
#>>> {1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 50, 20}

pop 그것은 단순히 사물의 모양에 의해 구현됩니다 : 그것은 목록을 가로 지르고 첫 번째를 팝합니다.


이것은 모든 구현 세부 사항입니다.


답변

“임의”는 “결정되지 않은”과 동일하지 않습니다.

그들이 말하는 것은 “공용 인터페이스에있는”사전 반복 순서의 유용한 속성이 없다는 것입니다. 현재 사전 반복을 구현하는 코드에 의해 완전히 결정되는 반복 순서의 많은 속성이 거의 확실하지만 저자는 사용할 수있는 것으로 약속하지 않습니다. 이를 통해 프로그램이 중단 될 염려없이 Python 버전간에 (또는 다른 운영 조건에서 또는 런타임에 완전히 무작위로) 이러한 속성을 자유롭게 변경할 수 있습니다.

따라서 모든 사전 순서의 모든 속성에 의존하는 프로그램을 작성 하면 사전 유형 사용에 대한 “계약을 위반하는 것”이며 Python 개발자는 이것이 작동하는 것처럼 보일지라도 이것이 항상 작동한다고 약속하지 않습니다 지금 당신이 그것을 테스트 할 때. 기본적으로 C에서 “정의되지 않은 행동”에 의존하는 것과 같습니다.


답변

이 질문에 대한 다른 답변은 훌륭하고 잘 쓰여져 있습니다. OP는 내가 “어떻게 도망 치는가”또는 “이유”로 해석하는 “어떻게”를 묻습니다.

파이썬 문서는 파이썬 사전이 추상 데이터 타입 연관 배열을 구현하기 때문에 사전 이 정렬되지 않았다고 말합니다 . 그들이 말하는 것처럼

바인딩이 반환되는 순서는 임의적 일 수 있습니다.

즉, 컴퓨터 과학 학생은 연관 배열이 주문되었다고 가정 할 수 없습니다. 수학 세트의 경우에도 마찬가지입니다.

세트의 요소가 나열된 순서는 관련이 없습니다.

컴퓨터 과학

집합은 특정 순서없이 특정 값을 저장할 수있는 추상 데이터 유형입니다.

해시 테이블을 사용하여 사전을 구현하는 것은 순서에 관한 한 연관 배열과 동일한 속성을 가지고 있다는 점에서 흥미로운 구현 세부 사항 입니다.


답변

파이썬 사용 해시 테이블 은 사전을 저장하기 을 하므로 사전이나 해시 테이블을 사용하는 다른 반복 가능한 객체에는 순서가 없습니다.

그러나 해시 객체의 항목의 인덱스에 대한 것은, 파이썬 코드를 다음에 따라 지수 계산 에서를hashtable.c :

key_hash = ht->hash_func(key);
index = key_hash & (ht->num_buckets - 1);

정수의 해시 값 자체는 정수로서 그 때문에, * 인덱스 번호에 기초은 ( ht->num_buckets - 1상수 임)에 의해 산출 된 인덱스 그래서 비트 AND 사이 (ht->num_buckets - 1)및 숫자 자체 * (-1 그것의 해시 인 기대 -2 ) 및 해시 값이있는 다른 객체의 경우

set해시 테이블을 사용 하는 다음 예제를 고려하십시오 .

>>> set([0,1919,2000,3,45,33,333,5])
set([0, 33, 3, 5, 45, 333, 2000, 1919])

숫자 33에는 다음이 있습니다.

33 & (ht->num_buckets - 1) = 1

실제로 그것은 :

'0b100001' & '0b111'= '0b1' # 1 the index of 33

참고 이 경우가 (ht->num_buckets - 1)있다 8-1=7거나0b111 .

그리고위한 1919:

'0b11101111111' & '0b111' = '0b111' # 7 the index of 1919

그리고위한 333:

'0b101001101' & '0b111' = '0b101' # 5 the index of 333

파이썬 해시 함수에 대한 자세한 내용은 파이썬 소스 코드 에서 다음 인용문을 읽는 것이 좋습니다 .

주요 미묘함 : 대부분의 해시 체계는 임의성을 시뮬레이션한다는 의미에서 “좋은”해시 함수를 사용하는 데 달려 있습니다. 파이썬은하지 않습니다 : 가장 중요한 해시 함수 (문자열 및 정수)는 일반적인 경우 매우 규칙적입니다.

>>> map(hash, (0, 1, 2, 3))
  [0, 1, 2, 3]
>>> map(hash, ("namea", "nameb", "namec", "named"))
  [-1658398457, -1658398460, -1658398459, -1658398462]

반드시 나쁘지는 않습니다! 반대로, 크기가 2 ** i 인 테이블에서는 하위 i 비트를 초기 테이블 인덱스로 사용하는 것이 매우 빠르며 연속 된 범위의 int로 인덱스 된 dict에 대해 충돌이 전혀 없습니다. 키가 “연속적인”문자열 인 경우에도 마찬가지입니다. 따라서 이것은 일반적인 경우보다 무작위보다 나은 동작을 제공하므로 매우 바람직합니다.

OTOH는 충돌이 발생할 때 해시 테이블의 연속 슬라이스를 채우는 경향이 좋은 충돌 해결 전략을 결정합니다. 해시 코드의 마지막 i 비트 만 사용하는 것도 취약합니다. 예를 들어, 목록 [i << 16 for i in range(20000)]을 키 세트로 고려하십시오 . int는 자체 해시 코드이므로 크기는 2 ** 15 크기이므로 모든 해시 코드의 마지막 15 비트는 모두 0입니다. 모두 동일한 테이블 인덱스에 매핑됩니다.

그러나 비정상적인 경우를 수용하면 일반적인 경우를 늦추지 않아야하므로 어쨌든 마지막 i 비트를 가져옵니다. 나머지를 수행하는 것은 충돌 해결에 달려 있습니다. 우리가 일반적으로 첫 번째 시도에서 찾고있는 키를 찾으면 (그리고 일반적으로 수행합니다-테이블로드 팩터는 2/3 이하로 유지되므로 확률은 우리에게 유리합니다) 초기 인덱스 계산 먼지를 저렴하게 유지하는 것이 가장 좋습니다.



* 클래스의 해시 함수 int:

class int:
    def __hash__(self):
        value = self
        if value == -1:
            value = -2
        return value


답변

파이썬 3.7부터 (그리고 이미 CPython 3.6에서 ) 사전 항목 은 삽입 된 순서대로 유지됩니다 .


답변