딕셔너리의 키가 없을 때의 접근
Effective Python의 Better way 16, 17, 18을 정리한 내용입니다.
딕셔너리로의 접근
가장 좋아하는 영화에 대해 투표를 받는 프로그램을 작성한다고 하자. 그럴 경우, 각 영화에 대한 투표수를 저장하는 딕셔너리가 필요할 것이다.
1
2
3
4
5
movie_votes = {
'해리포터': 3,
'반지의 제왕': 1,
'아이언맨': 2,
}
투표가 일어나서 딕셔너리를 변경시킬 때, 우리는 딕셔너리의 키가 존재하는지를 고려해야 한다.
1. 기본적인 방법
일반적으로, 아래와 같이 딕셔너리에 키가 있는지 명시적으로 확인 후 처리할 수 있다.
1
2
3
4
5
6
movie = '어벤져스'
if vote in movie_votes:
movie_votes[movie] += 1
else:
movie_votes[movie] = 1
이 방법의 경우, 딕셔너리에 두 번 접근하고, 한 번 대입하게 된다.
다른 방법도 존재한다. 키에 먼저 접근하고, 존재하지 않는 키라 예외가 발생한다면 이 예외를 처리하는 방법이다.
1
2
3
4
5
6
movie = '어벤져스'
try:
movie_votes[movie] += 1
except KeyError:
movie_votes[movie] = 1
이 방법은 이전 방법보다 효율적이다. 딕셔너리에 한 번만 접근하고, 한 번만 대입하기 때문이다. 하지만 두 방법 모두 필요 이상으로 길고 복잡해보인다.
2. dict.get 메소드
딕셔너리의 get 메소드를 사용하면 좀 더 짧고 가독성이 뛰어나게 코드를 작성할 수 있다.
1
2
3
4
movie = '어벤져스'
votes = movie_votes.get(movie, 0)
movie_votes[movie] = votes + 1
get의 두 번째 인자는 키가 존재하지 않을 때 가져올 default value로, 이렇게 사용했을 때 코드가 확연히 간단해졌음을 알 수 있다. 또한 예외를 처리하는 방법과 마찬가지로 딕셔너리에 한 번 접근하고 한 번 대입하게 되므로 효율적이다.
3. collections.defaultdict 클래스
dict.get 메소드 또한 상황에 따라 단점이 있을 수 있다. 바로 default 값이 모든 경우에 생성된다는 것이다. 영화 투표의 예시를 확장시켜 영화마다 투표한 사람의 이름이 추가되도록 하자.
1
2
3
4
5
6
7
8
9
movie_votes = {
'해리포터': ['이효준', '박태지'],
'반지의 제왕': ['신경호'],
'아이언맨': ['곽정무'],
}
movie = '어벤져스'
vote_names = movie_votes.get(movie, [])
vote_names.append('박준하')
이 경우, 이미 존재하는 키의 경우에도 여전히 새 리스트가 생성된다. 생성되는 비용이 크다면 성능에 꽤나 영향을 미치게 될 것이다.
defaultdict 클래스는 이 상황을 쉽게 해결해 준다.
1
2
3
4
5
6
from collections import defaultdict
movie_votes = defaultdict(list)
movie = '어벤져스'
movie_votes[movie].append('박준하')
defaultdict 클래스는 생성자에서 default 값을 생성해 줄 callable한 인자를 받는다. 이로써 defaultdict은 오직 필요할 때만 default 값을 생성하고 반환할 수 있게 된다.
4. __missing__ 매직 메소드
조금 더 복잡한 상황일 때, 위 방법들로 해결할 수 없는 경우가 있다. 이때는 __missing__ 매직 메소드를 사용하면 된다.
가령 영화 제목과 영화의 포스터 사진을 연결해 주는 딕셔너리를 만들고 싶다고 하자. 이 경우 default 값으로 포스터 사진을 반환하고 저장해야 한다.
(아래 예시의 경우, 포스터의 사진 대신 이에 접근할 수 있는 핸들을 저장하고 반환하도록 하였다.)
1
2
3
4
5
6
7
8
9
10
11
from collections import defaultdict
def open_picture(picture_path):
try:
return open(picture_path, 'rb')
except OSError:
print(f'경로를 열 수 없습니다. - {picture_path}')
raise
poster_handles = defaultdict(open_picture)
handle = poster_handles['path/to/poster']
defaultdict으로 쉽게 구현할 수 있는 것 처럼 보이지만 위 예시는 실행될 수 없다. defaultdict의 default 값을 생성해 주는 팩토리는 인자를 받지 않는다는 것이다.
이처럼 default 값을 생성할 때 key에 의존해야 하는 경우에는 dict를 상속받고 __missing__ 매직 메소드를 구현하자.
1
2
3
4
5
6
7
8
class PosterHandles(dict):
def __missing__(self, key):
value = open_picture(key)
self[key] = value
return value
poster_handles = PosterHandles()
handle = poster_handles['path/to/poster']
존재하지 않는 키에 접근하려고 하면 __missing__ 매직 메소드가 key 인자와 함께 호출된다. 우리는 여기서 적절한 default 값을 생성한 뒤, 저장하고 반환할 수 있다.