딕셔너리 삽입 순서에 의존할 때는 조심하라
09 Nov 2021
[Effective Python] Better way 15: 딕셔너리 삽입 순서에 의존할 때는 조심하라
- 파이썬 3.7부터는 dict 인스턴스에 들어 있는 내용을 이터레이션할 때 키를 삽입한 순서대로 돌려받는다는 사실에 의존 할 수 있다.
- 파이썬 dict은 아니지만 dict과 비슷한 객체를 쉽게 만들 수 있기 때문에 키 삽입 순서가 유지된다고 보장할 수 없다.
- dict과 비슷한 클래스를 조심스럽게 다루는 방법이 대표적으로 3가지가 있다.
-
python 3.5 이전에는 딕셔너리에 대해 이터레이션을 수행하면 키를 임의의 순서로 돌려주었고, 원소가 삽입된 순서와 일치 하지 않았습니다.
# python 3.5 버전 baby_names={'cat':'kitten','dog':'puppy'} print(baby_names) # {'dog': 'puppy', 'cat': 'kitten'}
이런 일이 발생하는 이유는 예전의 딕셔너리 구현이 내장 hash 함수와 파이썬 인터프리터가 시작할 때 초기화 된는 난수 seed을 사용하는 해시 테이블 알고리즘으로 만들어졌기 때문입니다. 인터프리터 실행마다 난수 seed값과 hash가 어우러 지면서 딕셔너리 순서가 삽입 순서와 일치 하지 않고, 프로그램 실행할 때마다 달라졌습니다.
-
python 3.6부터는 딕셔너리가 삽입순서를 보존하도록 동작이 개선되었고, python 3.7부터는 python 언어 명세에 내용이 포함되었습니다. 그래서 프로그래머가 생성한 순서대로 딕셔너리 내용을 표시합니다.
# python 3.6 버전이후 baby_names={'cat':'kitten','dog':'puppy'} print(baby_names) # {'cat': 'kitten', 'dog': 'puppy'}
-
python 3.6부터는 딕셔너리가 제공하는 모든 메서드에도 또한 삽입순서가 유지 됩니다.
print(list(baby_names.keys())) print(list(baby_names.values())) print(list(baby_names.items())) print(list(baby_names.popitem())) #마지막에 삽입된 원소 # ['cat', 'dog'] # ['kitten', 'puppy'] # [('cat', 'kitten'), ('dog', 'puppy')] # ['dog', 'puppy']
-
이런 순서가 유지되는 변경은 dict 타입과 이 타입의 특정 구현에 의존하는 여러 다른 파이썬 기능에 수많은 영향을 끼쳤고, 대표적인 것이 함수에 대한 키워드 인자입니다. 현재는 키워드 인자의 순서가 프로그래머가 함수를 호출할 때 사용하는 인자 순서와 일치합니다.
def func(**kwargs): for key,value in kwargs.items(): print(f'{key}={value}') func(goose='gosling',kangaroo='joey') # goose=gosling # kangaroo=joey
-
클래스의 인스턴스 딕셔너리에 대해서도 각 인스턴스 필드를 대입한 순서가 유지 됩니다.
class MyClass: def __init__(self): self.alligator = 'hatchling' self.elephant = 'calf' a = MyClass() for key,value in a.__dict__.items(): print(f'{key}:{value}') # alligator:hatchling # elephant:calf
-
딕셔너리가 삽입 순서를 유지하는 방식은 이제 파이썬 언어 명세의 일부가 됬습니다. 하지만 딕셔너리를 처리할 때 앞에서 설명한 삽입 순서 관련 동작이 항상 성립한다고 가정하며 안됩니다. 파이썬에서는 프로그래머가list, dict 등의 표준 프로토콜을 변경한 커스텀 컨테이너 타입을 쉽게 정의할 수 있습니다.
파이썬은 정적 타입 지정 언어가 이니 때문에 대부부느이 경우 코드는 엄격한 계층 보다는 객체의 동작이 객체의 실질적인 타입을 결정하는 덕 타입핑에 의존합니다.
votes = { 'otter':1281, 'polar bear': 587, 'fox' : 863 } def populate_ranks(votes,ranks): names = list(votes.keys()) names.sort(key=votes.get, reverse = True) for i,name in enumerate(names,1): ranks[name]=i def get_winner(ranks): return next(iter(ranks)) ranks={} populate_ranks(votes,ranks) print(ranks) winner = get_winner(ranks) winner = get_winner(ranks) print(winner) # {'otter': 1, 'fox': 2, 'polar bear': 3} # otter
위 처럼 정상적으로 숫자가 가장 높은 동물이 출력됩니다.
collections.abc
모듈을 사용하여 딕셔너리와 비슷하지만 내용을 알파벳 순서대로 iteration 해주는 클래스를 새로 정의 할 수 있습니다.from collections.abc import MutableMapping class SortedDict(MutableMapping): def __init__(self): self.data={} def __getitem__(self, item): return self.data[key] def __setitem__(self,key,value): self.data[key] = value def __delitem__(self, key): del self.data[key] def __iter__(self): keys = list(self.data.keys()) keys.sort() for key in keys: yield key def __len__(self): return len(self.data) sorted_ranks = SortedDict() populate_ranks(votes,sorted_ranks) print(sorted_ranks.data) winner = get_winner(sorted_ranks) print(winner) # {'otter': 1, 'fox': 2, 'polar bear': 3} # fox
get_winner의 구현이 populate_ranks의 삽입 순서에 맞게 딕셔너리를 이터레이션 한다고 가정하였기 때문에, 원하는 결과가 나오지 않습니다.
-
3가지 해결방법이 있습니다. 첫번째 방법은 ranks 딕셔너리가 어떤 특정 순서로 이터레이션 된다고 가정하지 않고 get_winner함수를 그냥 구현하는 것입니다.
def get_winner(ranks): for name,rank in ranks.items(): if rank ==1: return name winner = get_winner(sorted_ranks.data) print(winner) # otter
-
두번째 방법은 맨 함수앞에 ranks의 타입이 우리가 원하는 타입인지 검사하는 코드를 추가하는 것입니다.
def get_winner(ranks): if not isinstance(ranks,dict): raise TypeError('dict 인스턴스가 필요합니다') return next(iter(ranks)) get_winner(sorted_ranks) # TypeError: dict 인스턴스가 필요합니다
-
세번째 방법은 타입 annotation을 사용해서 get_winner에 전달되는 값이 딕셔너리와 비슷한 동작을 하는 MutableMapping 인스턴스가 아니라 dict 인스턴스가 되도록 강제하는 것 입니다.
from typing import Dict,MutableMapping def populate_ranks(votes:Dict[str,int], ranks:Dict[str,int])->None: names = list(votes.keys()) names.sort(key=votes.get,reverse=True) for i, name in enumerate(name,i): ranks[name]=i def get_winner(ranks:Dict[str,int])->str: return next(iter(ranks)) sorted_ranks=SortedDict() populate_ranks(votes,sorted_ranks) print(sorted_ranks.data) winner=get_winner(sorted_ranks.data) print(winner) # bw15.py:112: error: Call to untyped function "SortedDict" in typed context # bw15.py:113: error: Argument 2 to "populate_ranks" has incompatible type "SortedDict"; expected "Dict[str, int]"