Effective Python C2. Dictionary 원소가 없을 때 처리 방법

5 분 소요

Introduction

dictionary를 사용할 때 key 또는 값이 없을 경우 예외처리를 해야한다.
Effective python의 경우는 아래와 방법들을 제안한다.

  1. key가 존재하지 않을 경우, 디폴트 값 반환 : get
  2. key가 존재하지 않을 경우, 디폴트 값을 설정하고 반환 : setdefault > 책에서는 리소스 소모 문제로 추천하지 않음.
  3. key가 없는 default값을 일괄적으로 정하고 싶은 경우 : defaultdict
  4. key가 없는 경우, key에 따른 디폴트 값 설정 : dict 상속 후, missing

Best WAY 16 : 딕셔너리 키가 없을 때 Key Error를 처리하기 보다는 get을 사용하라.

get의 사용

물건 이름 리스트를 키로 가지며, 물건 개수를 값으로 가지는 counter란 딕셔너리를 만든다.

counter = {
    '사과': 2,
    '빵': 1
}

이제 counter에서 가져올 물건에 해당하는 키가 없는 경우에는 0을 반환하도록 해보자.
in을 사용하면 다음과 같이 짤 수 있다.

key = '책'
if key in counters:
    count = counters[key]
else:
    count = 0

counters[key] = count + 1

또는 try-except문과 함께 KeyError를 사용할 수 있으나, 매번 이런 방법들을 쓰기에는 좀 지저분해보일 수 있다.
아래와 같이 get을 사용해 가져올 키-값 쌍이 없는 경우, 기본값을 지정해 가져올 수 있다.

count = counters.get(key, 0)
counters[key] = count + 1

setdefault

만약 기본값을 가져오는 것 뿐 아니라 해당 딕셔너리에 반영하고 싶다면, setdefault를 쓸 수 있다.

아래 예제는 딕셔너리가 값을 리스트로 가지는 상황이다. 특정 키에 대한 값을 가져오되, 해당하는 키가 딕셔너리에 존재하지 않는 경우 리스트로 디폴트 값을 설정한다.

단, 이 경우 setdefault는 호출할 때마다 리스트를 만들어야 하므로 성능이 저하될 수 있다. 이런 이유로 setdefault는 effective python에서 그렇게 추천하지 않는다.

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

# in과 대입문만 사용할 경우 
if key in votes:
    names = votes[key]
else:
    votes[key] = names = []
    
names.append(who)
print(votes)

# get과 왈러스 연산자를 활용할 경우
if(name := votes.get(key)) is None:
    votes[key] = names = []
    
names.append(who)

# setdefault를 사용할 경우
names = votes.setdefault(key, [])
names.append(who)

>>>
{'바게트': ['철수', '순이'], '치아바타': ['하니', '유리'], '브리오슈': ['수영']}

Best WAY 17 : 내부 상태에서 원소가 없는 경우를 처리할 때는 setdefault보다 defaultdict을 사용하라

key값이 없는 딕셔너리 원소의 기본값을 미리 설정해줄 수도 있다.

아래 나라별로 방문한 지역을 저장하는, Visits class의 data 딕셔너리 기본값을 빈 set 으로 설정해두었으므로 key를 설정하지 않은 ‘영국’, ‘독일’의 지역이 원소로 바로 추가될 수 있다.

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)

Best WAY 18 : __missing__을 사용해 키에 따라 다른 디폴트 값을 생성하는 방법을 알아두라

defaultdict은 원소가 없는 키에 대해 일괄적으로 같은 기본값을 설정하는 것이다. 인자를 받지 않기 때문에, 접근에 사용한 키에 맞는 디폴트 값을 생성할 수 없다. 만약 키가 없을 때에 처리를 위한 커스텀 로직을 만들 경우 __missing__을 사용하라.

def open_picture(profile_path):
    try:
        return open(profile_path, 'a+b')
    except OSError:
        print(f'경로를 열 수 없습니다. : {profile_path}')
        raise
    
    
class Pictures(dict):
    def __missing__(self, key):
        value = open_picture(key)
        self[key] = value
        return value
    
path = 'profile_1234.jpg'
    
pictures = Pictures()
handle = pictures[path]
handle.seek(0)
image_data = handle.read()

Summary

  • 카운터와 같이 기본적인 타입의 값이 들어가는 딕셔너리를 다룰 때는 get이 가장 좋다.
  • key가 없을 때의 디폴트 값을 설정하려면 defaultdict을 이용한다.
  • 디폴트 키를 만들 때 어떤 키를 사용했는지 알아야 하면, 직접 dict을 상속받은 하위 클래스와 __missing__을 정의하면 된다.

Reference

파이썬 코딩의 기술 제 2판. - 브렛 슬리킨 지음 / 오현석 옮김