딕셔너리와 상호작용하는 세 가지 기본 연산(Key와 Value에 접근, 대입, 삭제)을 수행할 때 동적인 딕셔너리의 내용으로 인해 키에 접근하거나 삭제할 때 그 키가 딕셔너리에 없을 경우가 종종 발생한다.
counters = {
'품퍼니켈' : 2,
'사워도우' : 1,
}
위와 같은 딕셔너리에서 투표가 일어날 때 카운터를 증가시키려면 먼저 키가 딕셔너리에 존재하는지 살펴봐야 한다. 키가 없으면 디폴트 카운터 값인 0을 딕셔너리에 넣고 그 카운터를 증가시킨다.
key = '밀'
if key in counters:
count = counters[key]
else:
count = 0
counters[key] = count + 1
→ 딕셔너리에서 키를 두 번 읽고, 키에 대한 값을 한 번 대입해야 한다, 비효율적이다.
try:
count = counters[key]
except KeyError:
count = 0
counters[key] = count + 1
→ 키를 한 번만 읽고 값을 한 번만 대입하면 되므로 in에 비해 효율적이나 get 메소드 활용에 비해 비효율적이다.
#방법 1
if key not in counters:
counters[key] = 0
counters[key] += 1
#방법 2
if key in counters:
counters[key] += 1
else:
counters[key] = 1
#방법 3
try:
counters[key] += 1
except KeyError:
counters[key] = 1
→ 대입을 중복 사용해야 하므로 코드의 가독성이 떨어지므로 비효율적이다.
count = counters.get(key,0)
#get의 두 번재 인자는 딕셔너리에 key가 없을 때 돌려줄 디폴트 값
counters[key] = count + 1
→ 키를 한 번만 읽고 값을 한 번만 대입한다, KeyError 방법에 비해 코드가 짧다. 가장 효율적이고 가독성을 높일 수 있는 방법이다.
collection내장 모듈의 Counter클래스
카운터로 이뤄진 딕셔너리를 유지해야 하는 경우에는 collection 내장 모듈의 Counter클래스를 사용하는 것을 고려해보는 것도 좋을 것 같다.
Collections 모듈 - Counter
votes = {
'바게트' : ['철수', '순이'],
'치아바타' : ['하니', '유리'],
}
key = '브리오슈'
who = '단이'
if key in votes:
names = votes[key]
else:
votes[key] = names = []
names.append(who)
print(votes)
>>>
{'바게트': ['철수', '순이'], '치아바타': ['하니', '유리'], '브리오슈': ['단이']
→ 딕셔너리에서 키를 두 번 읽고, 키가 없는 경우 값을 한 번 대입하므로 비효율적이다.
votes[key] = names = []
a = b = c 와 같은 형태의 이중 대입문은 a와 b가 c 값을 가진다는 의미이다.
ex) a = b = c = 2
a=2, b=2, c=2
KeyError 예외를 활용하는 방법
try:
names = votes[key]
except KeyError:
votes[key] = names = []
names.append(who)
→ 키를 한 번만 읽고 값을 한 번만 대입하면 되므로 in을 활용한 방법에 비해 효율적이나, get 메소드 활용에 비해 비효율적이다.
names = votes.get(key)
if names is None:
votes[key] = names = []
names.append(who)
추가적으로 BetterWay10. '대입식을 사용해 반복을 피하라' 에서 소개된 왈러스 연산자를 사용하면 더 짧게 쓸 수 있다.
if (names := votes.get(key)) is None:
votes[key] = names = []
names.append(who)
→ 키를 한 번만 읽고 값을 한 번만 대입한다, KeyError 방법에 비해 코드가 짧다. 가장 효율적이고 가독성을 높일 수 있는 방법이다.
위 패턴을 더 간단히 사용할 수 있게 해주는 dict 타입의 setdefault 메서드가 있다.
딕셔너리에서 키를 사용해 값을 가져오려고 시도하고, 키가 없으면 제공받은 디폴트 값을 키에 연관시켜 딕셔너리에 대입한 다음 키에 연관된 값을 반환한다.
names = votes.setdefault(key, [])
names.append(who)
→ get과 대입식을 사용하는 것 보다 짧지만 저자는 set이라는 단어가 위 패턴의 동작을 잘 설명하지 못하는 단어라 가독성이 좋지 않다고 판단한다.
data = {}
key = 'foo'
value = []
data.setdefault(key, value)
print('이전:', data)
value.append('hello')
print('이후:', data)
>>>
이전: {'foo': []}
이후: {'foo': ['hello']}
→ 키가 없으면 setdefault에 전달된 디폴트 값이 별도로 복사되지 않고 딕셔너리에 직접 대입된다.
→ 호출할 때마다 리스트를 만들어야 해서 성능 저하의 여지가 있다.
→ setdefault 대신 defaultdict 메서드를 사용할 것을 고려해 보아야 한다.