딕셔너리는 순서 있는 매핑

파이썬 3.7 부터 표준 딕셔너리 dict 가 삽입 순서를 보존합니다. 다음과 같은 코드가 항상 같은 결과를 준다는 뜻입니다.

>>> d = {}
>>> d['b'] = None
>>> d['a'] = None
>>> list(d)
['b', 'a']

이터레이터는 먼저 삽입된 키를 먼저 줍니다. 3.7 이전에 같은 결과가 나왔다면 그저 우연일 뿐이라는 뜻입니다. 값을 변경하는 것은 순서에 영향을 주지 않습니다.

>>> d['b'] = 1
>>> list(d)
['b', 'a']

키를 삭제해도 남은 것들의 순서는 여전히 보존됩니다.

>>> d['c'] = None
>>> list(d)
['b', 'a', 'c']
>>> del d['a']
>>> list(d)
['b', 'c']

일단 지운 키를 다시 삽입하면 끝으로 들어갑니다.

>>> d['a'] = None
>>> list(d)
['b', 'c', 'a']

리터럴이나 컴프리헨션도 마찬가지 입니다.

>>> list({'b': None, 'a': None})
['b', 'a']
>>> list({k:None for k in 'ba'})
['b', 'a']

하지만 키 삽입 순서가 비교에 영향을 주지는 않습니다.

>>> {'a':None, 'b':None} == {'b':None, 'a':None}
True

이 것은 collections.OrderedDict 와 다른 동작입니다.

>>> dict.fromkeys('ab') == dict.fromkeys('ba')
True
>>> OrderedDict.fromkeys('ab') == OrderedDict.fromkeys('ba')
False

사실 이런 동작은 CPython 3.6 구현부터 시작되었습니다. 다만 언어 정의는 딕셔너리의 이터레이터가 돌려주는 키의 순서가 정의되지 않았다고 말하고 있었기 때문에, 이렇게 동작하는 것을 알고 있음에도 불구하고, 키의 삽입 순서가 보존될 필요가 있을 때는 딕셔너리를 쓰지 못하고 collections.OrderedDict 를 써야만 했습니다.

사실 CPython 외에는 파이썬 3.6 을 구현한 대안 구현은 존재하지 않습니다. PyPy 도 3.5 까지 구현하고 있을뿐입니다. 더군다나 PyPy 의 딕셔너리는 이미 오래전에 이런 동작을 구현하고 있습니다. 아마도 파이썬 3.5 도 지원하는 이식성있는 코드를 작성할 때는 여전히 collections.OrderedDict 를 써야만 하겠지만 파이썬 3.6 이상에서는 dict 로 대체해도 문제가 되지 않습니다.

이런 변화는 언어 정의상의 모호함을 일부 해소합니다. 현재 파이썬 3.6 의 파이썬 언어 레퍼런스 는 두 곳에서 dict 의 순서 보존 성질을 활용하고 있습니다.

키워드 인자의 순서 보존

다음과 같은 코드를 봅시다.

>>> def f(**kwargs):
...    return kwargs
...
>>> list(f(b=1, a=2))
['b', 'a']

파이썬 언어 레퍼런스함수 정의 섹션을 보면 다음과 같은 정의가 나옵니다.

"**identifier" 형태가 존재하면, 남는 키워드 인자들을 받는 순서 있는 매핑으로 초기화된다.

kwargs 인자는 순서 있는 매핑이라는 뜻입니다. 따라서 이터레이터는 카워드 인자를 호출자가 제공한 순서대로 전달애야만 하고, 위와 같은 결과가 나와야 합니다. 하지만 실제 전달되는 kwargs 의 형은 dict 입니다.

>>> type(f(b=1, a=2))
<class 'dict'>

"순서 있는" 이라는 문구는 3.5 문서에는 등장하지 않습니다. 더 자세한 내용은 PEP 468 을 참고하세요.

클래스 이름 공간의 순서 보존

파이썬 언어 레퍼런스클래스 정의 섹션을 보면 이런 구절이 나옵니다.

클래스 바디에서 어트리뷰트가 정의되는 순서는, 새 클래스의 __dict__ 에 보존된다. 이것은 클래스가 만들어진 직후에, 정의 문법을 사용해서 정의되는 클래스들에서만 신뢰할 수 있다는 것에 주의해야 한다.

또한, 메타클래스와 관련된 클래스 이름 공간 준비하기 섹션을 보면 이런 구절이 나옵니다.

만약 메타 클래스에 __prepare__ 어트리뷰트가 없다면, 클래스 이름 공간은 빈 순서 있는 매핑으로 초기화된다.

결국 클래스 어트리뷰트의 순서도 보존되고, 메타 클래스에서 자주 사용하던 다음과 같은 트릭이 더이상 필요 없는 쪽으로 바뀌고 있다는 뜻입니다.

class OrderedClass(type):

    @classmethod
    def __prepare__(metacls, name, bases, **kwds):
        return collections.OrderedDict()

역시 "순서 있는" 이라는 문구는 3.5 문서에는 등장하지 않습니다. 더 자세한 내용은 PEP 520 을 참고하세요 (대부분의 내용은 세상에 나와보지도 못하고 사라진 __definition_order__ 에 관한 것이라 큰 도움이 되지 않을 수도 있습니다.).

그러면 이제 collections.OrderedDict 는 아무런 쓸모가 없는 것일까요? 쓸모가 그리 크지 않다는 것은 분명하지만 dict 와 차이를 보이는 부분도 존재합니다.

하지만 꼭 바꿔야 한다면 dict 로 변경하는데 큰 무리가 없습니다. 특히 collections.OrderedDictdict 에 비해 메모리를 두 배 정도 사용합니다.

끝으로 덧붙이자면, 이런 변화는 dict 에만 있을 뿐, set 은 여전히 삽입 순서를 보존하지 않습니다.

>>> s = set()
>>> s.add('b')
>>> s.add('a')
>>> list(s)
['a', 'b']

경우에 따라서는 ['b', 'a'] 가 출력될 수도 있습니다.