dictionary 완벽 정복!

About_work·2023년 1월 18일
0

python 기초

목록 보기
10/65

dictionary

언제 써야해?

  • 동적 데이터를 관리하고 싶을 떄
  • 이유
    • hash table 이나 associate array 라고 불리는 데이터 구조 안에 값을 저장
    • (분할상환 복잡도 로) 상수 시간에 원소를 삽입하거나 찾을 수 있다.

언제 쓰지 말아야 해?

  • (dictionary, long tuple, 다른 내장 타입이 복잡하게 내포된 데이터를 값으로 사용하는) dictionary를 만들지 말라.
  • 내부 상태를 표현하는 dictionary가 복잡해지면, 이 데이터를 관리하는 코드를 여러 클래스로 나눠서 재작성하라.
  • dictionary 안에 dictionary를 포함시키지 말라. 코드가 읽기 어려워지고, 여러분 스스로도 유지 보수의 ‘악몽’ 속으로 들어가는 셈이다.
  • 어떠 내장 타입이던 내포 단계가 2 단계 이상 되면, 더 이상 dictionary, list, tuple 계층을 추가하지 말라. ( 2단계 까지 허용)

기초

  • dictionary

    • hash table 이나 associate array 라고 불리는 데이터 구조 안에 값을 저장한다.
    • (분할상환 복잡도 로) 상수 시간에 원소를 삽입하거나 찾을 수 있다.
    • 따라서 동적인 정보를 관리하는 데는 dictionary가 가장 이상적이다.
  • collections.OrderedDict 를 언제 써야해?

    • key 삽입할 일이 매우 많을 떄
    • popitem() 호출이 매우 많을 때
  • collections.Counter

    • value가 카운터로 이뤄진 dictionary의 경우에는 위 클래스의 사용을 고려해보라.

찾으려는 key가 없는 경우에 대응하기 위해, dictionary.get() 을 많이 써라.

  • value = dict.get(key, default_value)

    • dictionary의 key에 해당하는 value 값을 가져오되, key가 없으면 default_value를 가져오라.
    • get을 사용하는 방법은 코드가 짧고 깔끔하다.
  • 하지만, value가 list인 딕셔너리에 get 을 이용한다면? 아래와 같이 해라.

votes = {
    '바게트': ['철수', '순이'],
    '치아바타': ['하니', '유리'],
}
key = '브리오슈'
who = '단이'

if (names := votes.get(key)) is None:
    votes[key] = names = []
names.append(who)

setdefault 보다는 collections.defaultdict 를 사용하라.

setdefault

  • 장점
    • 코드가 짧아진다.
  • 단점
    • 함수 이름이 직관적이지 않다.(동작을 이해하기 어럽다.)
  • 언제 사용해?
    • default 값을 만드는 계산 비용이 높거나, 만드는 과정에서 에러가 발생할 수 있는 경우에는 사용하지 말라.
    • 꼭 필요한 경우 말고는 사용 지양.
visits = {
    '미국': {'뉴욕', '로스엔젤레스'},
    '일본': {'하코네'},
}

visits.setdefault('프랑스', set()).add('칸') # 짧다

# 하지만 setdefault 를 안쓰면? 길다.
if (japan := visits.get('일본')) is None:
    visits['일본'] = japan = set()

japan.add('교토')

print(visits)

collections.defaultdict

  • 아래 예시에서, add 코드는 data 딕셔너리에 없는 key에 접근하면 항상 기존 set 인스턴스가 반환된다고 가정한다.
  • 단점
    • 접근에 사용한 key에 맞는 default value를 생성하는 것은 불가능하다.
from collections import defaultdict
class Visits:
    def __init__(self):
        self.data = defaultdict(set)

    def add(self, country, city):
        self.data[country].add(city)

visits = Visits()
visits.add('영국', '바스')
visits.add('영국', '런던')
print(visits.data)
>>>
defaultdict(<class 'set'>, {'영국': {'바스', '런던'}})

__missing__을 사용해 키에 따라 다른 default 값을 생성하는 방법을 알아두라.

  • dict 타입의 하위 클래스를 만들고 & __missing__ special method를 구현하면, key가 없는 경우를 처리하는 로직을 custom 화 할 수 있다.
  • 장점
    • 새 key의 default value를 만들 때, 새 key를 반드시 알아야 하는 상황이라면, 유용하다.

class Pictures(dict):
    def __missing__(self, key):
        value = open_picture(key)
        self[key] = value
        return value

pictures = Pictures()
handle = pictures[path]
handle.seek(0)
image_data = handle.read()

custom 딕셔너리 삽입 순서에 의존할 때는 조심하라. (크게 중요하진 않음)

  • 파이썬에서는 프로그래머가 list, dict 등의 표준 protocol을 흉내 내는 custom container 타입을 쉽게 정의할 수 있다.
  • 컨테이너 ??
    • __contain__ 메서드가 구현되어있는 객체
    • 자료형에 상관없이 저장이 가능한 객체
    • 여러 데이터 객체에 대한 메모리 참조 정보를 담고 있는 객체
    • 문자열(str), 튜플(tuple), 리스트(list), 딕셔터리(dictionary), 집합(set) 등은 타입에 무관하게 저장이 가능한 컨테이너 객체
    • 정수, 실수, 복소수 등은 타입이 고정되어 있는 단일 종류(Literal)한 자료형
  • 파이썬은 대부분의 경우 코드는 엄격한 클래스 계층보다는, 객체의 동작이 객체의 실질적인 type을 결정하는 duck typing에 의존
  • 아래 예시는, 동물 인기 투표 순위 오름차순 정렬 예시이다.
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))

votes = {
    'otter': 1281,
    'polar bear': 587,
    'fox': 863,
}
ranks = {}
populate_ranks(votes, ranks)
print(ranks)
winner = get_winner(ranks)
print(winner)
  • 갑자기 프로그램 요구 사항이, 인기 투표 순위 -> 이름파벳 순으로 바뀐다면?
    • ranks에 dictionary 말고, Sorted Dictionary로 바뀐다면?
from collections.abc import MutableMapping

class SortedDict(MutableMapping):
    def __init__(self):
        self.data = {}

    def __getitem__(self, key):
        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)
  • 이렇게 하면 get_winner 에서 원하는 결과가 안나오는데, 그 이유는 get_winner의 구현 자체가 populate_ranks의 삽입 순서에 맞게 dictionary를 iteration 한다고 가정하는데 있다.
  • 이를 해결하려면,
    • 1: get_winner 함수 구현을, ranks가 어떤 특정 순서로 iteration 된다고 가정하지 않는다.
    • 2: get_winner 함수의 ranks argument를 type 체크하도록 코딩한다.
    • 3: type annotation을 통해 ranks argument가 dict 인스턴스가 되도록 강제한다.
profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글