딕셔너리란 무엇인가?
안녕하세요. 오늘은 파이썬 프로그래밍에서 가장 많이 사용되는 자료구조 중 하나인 딕셔너리(Dictionary)에 대해 자세히 알아보겠습니다. 딕셔너리는 키(key)와 값(value) 쌍으로 이루어진 자료구조입니다. 딕셔너리를 잘 활용하면 데이터를 효율적으로 저장하고 관리할 수 있습니다.
딕셔너리는 무엇일까요? 딕셔너리는 순서가 없는(unordered) 키와 값의 쌍으로 이루어진 자료구조입니다. 딕셔너리의 키는 중복될 수 없으며 숫자, 문자열, 튜플 등의 불변 자료형을 사용할 수 있습니다. 값은 어떤 자료형이든 가능합니다. 딕셔너리의 가장 큰 특징은 키를 이용해 빠르게 값을 찾을 수 있다는 점입니다.
딕셔너리는 상당히 유연한 자료구조이며, 실생활에서 데이터를 표현하는 데 딱 맞는 구조라고 볼 수 있습니다. 딕셔너리의 용도는 다음과 같습니다.
- 데이터를 키-값 쌍으로 저장할 때 사용
- 객체의 속성과 값을 표현할 때 사용
- 데이터 간 연관관계를 표현할 때 사용
딕셔너리 생성하기
딕셔너리는 {} 중괄호를 이용하거나 dict() 함수를 이용해 생성할 수 있습니다.
# 중괄호를 이용한 딕셔너리 생성
점수 = {'영어': 90, '수학': 85, '국어': 92}
# dict() 함수를 이용한 딕셔너리 생성
학생 = dict(이름='김철수', 나이=17, 학년=2)
print(점수) # {'영어': 90, '수학': 85, '국어': 92}
print(학생) # {'이름': '김철수', '나이': 17, '학년': 2}
딕셔너리에 초기값을 지정하지 않으면 빈 딕셔너리 {}가 생성됩니다.
왜 딕셔너리를 사용할까요? 딕셔너리는 키를 통해 값을 빠르게 찾을 수 있습니다. 예를 들어 학생 정보를 딕셔너리로 저장하면 키인 '이름', '나이' 등으로 쉽게 값에 접근할 수 있습니다. 또한 데이터의 구조를 잘 나타낼 수 있어 프로그래밍에서 널리 사용됩니다.
딕셔너리 값 접근하기
딕셔너리의 값에 접근하는 방법은 딕셔너리[키] 형태로 하는 것입니다.
학생 = {'이름': '김철수', '나이': 17, '학년': 2}
print(학생['이름']) # 김철수
print(학생['나이']) # 17
print(학생['학년']) # 2
키가 딕셔너리에 없으면 KeyError가 발생하므로 주의해야 합니다. 이 경우 get() 메서드를 사용하면 더 안전하게 값을 가져올 수 있습니다.
점수 = {'영어': 90, '수학': 85}
print(점수.get('국어', 0)) # 0 (국어 키가 없으면 0 반환)
get(키, 기본값) 메서드는 키가 없을 때 미리 지정한 기본값을 반환합니다. 기본값을 지정하지 않으면 None을 반환합니다.
딕셔너리 값 할당하기
딕셔너리에 새로운 키-값 쌍을 추가하거나 기존 값을 수정할 수 있습니다.
학생 = {'이름': '김철수', '나이': 17, '학년': 2}
# 새로운 키-값 쌍 추가
학생['전공'] = '컴퓨터공학'
print(학생) # {'이름': '김철수', '나이': 17, '학년': 2, '전공': '컴퓨터공학'}
# 기존 값 수정
학생['나이'] = 18
print(학생) # {'이름': '김철수', '나이': 18, '학년': 2, '전공': '컴퓨터공학'}
딕셔너리에 키가 없으면 새로운 키-값 쌍이 추가되고, 키가 이미 있으면 값이 수정됩니다.
또한 update() 메서드를 이용해 여러 키-값 쌍을 한꺼번에 추가하거나 수정할 수도 있습니다.
새정보 = {'주소': '서울', '핸드폰': '010-1234-5678'}
학생.update(새정보)
print(학생)
# {'이름': '김철수', '나이': 18, '학년': 2, '전공': '컴퓨터공학', '주소': '서울', '핸드폰': '010-1234-5678'}
딕셔너리 값 삭제하기
딕셔너리에서 특정 키-값 쌍을 삭제하려면 pop() 메서드나 del 키워드를 사용합니다.
학생 = {'이름': '김철수', '나이': 18, '전공': '컴퓨터공학'}
# pop() 메서드 사용
나이 = 학생.pop('나이')
print(나이) # 18
print(학생) # {'이름': '김철수', '전공': '컴퓨터공학'}
# del 키워드 사용
del 학생['전공']
print(학생) # {'이름': '김철수'}
pop(키, 기본값) 메서드는 딕셔너리에서 해당 키-값 쌍을 제거하고 값을 반환합니다. 키가 없으면 지정한 기본값을 반환하며, 기본값을 지정하지 않으면 KeyError를 발생시킵니다.
del 딕셔너리[키]는 딕셔너리에서 해당 키-값 쌍을 완전히 삭제합니다. 존재하지 않는 키를 지정하면 KeyError가 발생합니다.
참고로 전체 딕셔너리를 한꺼번에 삭제하고 싶다면 clear() 메서드를 사용하면 됩니다.
학생 = {'이름': '김철수', '나이': 18}
학생.clear()
print(학생) # {}
딕셔너리 순회하기
딕셔너리의 모든 키-값 쌍에 접근하려면 반복문을 사용합니다. 딕셔너리는 순서가 없는 자료형이므로 키를 순서대로 정렬하여 출력하려면 별도의 처리가 필요합니다.
학생 = {'이름': '김철수', '나이': 18, '학년': 2, '전공': '컴퓨터공학'}
# 키 순회
for 키 in 학생:
print(키, end=' ') # 이름 나이 학년 전공
# 키-값 쌍 순회
for 키, 값 in 학생.items():
print(키, 값)
# 이름 김철수
# 나이 18
# 학년 2
# 전공 컴퓨터공학
# 값만 순회
for 값 in 학생.values():
print(값, end=' ') # 김철수 18 2 컴퓨터공학
위 예시에서 알 수 있듯이, 딕셔너리에서 items() 메서드를 사용하면 키-값 쌍을 모두 가져올 수 있습니다. 반면 keys() 메서드는 키만, values() 메서드는 값만 가져옵니다.
정렬된 순서로 출력하려면 어떻게 해야 할까요? sorted(딕셔너리) 함수를 사용하면 키 값을 정렬하여 리스트로 반환합니다. 이를 이용해 정렬된 순서로 딕셔너리를 순회할 수 있습니다.
학생 = {'이름': '김철수', '나이': 18, '학년': 2, '전공': '컴퓨터공학'}
for 키 in sorted(학생):
print(f"{키}: {학생[키]}")
# 나이: 18
# 이름: 김철수
# 전공: 컴퓨터공학
# 학년: 2
값을 기준으로 정렬하고 싶다면 sorted(딕셔너리.items(), key=lambda x: x[1])과 같이 items()와 key 인수를 함께 사용하면 됩니다.
딕셔너리 내포
리스트 내포와 마찬가지로 딕셔너리도 내포 방식을 사용할 수 있습니다. 이를 통해 간결하게 딕셔너리를 생성할 수 있습니다.
# 제곱 딕셔너리 생성
제곱 = {x: x**2 for x in range(6)}
print(제곱) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# 두 리스트로부터 딕셔너리 생성
이름 = ['철수', '영희', '민수', '경희']
나이 = [23, 25, 27, 22]
학생 = {이름: 나이 for 이름, 나이 in zip(이름, 나이)}
print(학생) # {'철수': 23, '영희': 25, '민수': 27, '경희': 22}
딕셔너리 내포는 간결한 코드를 작성할 수 있어 유용합니다. zip() 함수를 사용하면 두 개 이상의 반복 가능한 자료형을 쌍으로 묶을 수 있습니다.
딕셔너리 이해와 최적화
딕셔너리는 효과적인 자료구조이지만 몇 가지 주의사항이 있습니다.
첫째, 키는 해시 가능한 불변 자료형이어야 합니다. 따라서 리스트나 딕셔너리는 키로 사용할 수 없습니다.
딕셔너리 = {}
키1 = [1, 2]
키2 = (1, 2)
키3 = "문자열"
딕셔너리[키1] = 100 # 에러 발생: unhashable type
딕셔너리[키2] = 200 # 성공
딕셔너리[키3] = 300 # 성공
둘째, 키 검색 시간은 일정한 O(1)의 시간 복잡도를 가집니다. 하지만 리스트 등은 O(n)의 시간복잡도를 가지므로 딕셔너리가 더 효율적입니다. 그래서 딕셔너리는 매우 큰 데이터를 다루는 상황에서도 유용합니다.
그러나 딕셔너리의 주요 동작(삽입, 삭제, 검색)은 평균적으로 O(1)의 시간 복잡도를 가지지만, 최악의 경우 O(n)의 시간복잡도를 가질 수 있습니다. 이는 해시 충돌 때문입니다.
해시 충돌(Hash Collision)이란 무엇일까요? 파이썬 딕셔너리는 내부적으로 해싱(Hashing) 기법을 사용하여 키를 저장합니다. 해싱은 키를 고정 크기의 값으로 매핑하는 방식입니다.
하지만 서로 다른 키가 동일한 해시값으로 매핑되는 해시 충돌이 발생할 수 있습니다. 이 경우 해당 키들을 따로 저장해야 하므로 검색 시간이 O(n)으로 증가합니다.
오픈 어드레싱(Open Addressing) 기법
파이썬은 해시 충돌을 최소화하기 위해 오픈 어드레싱 기법을 사용합니다. 이 기법은 충돌이 발생했을 때, 해시 테이블 내의 다른 버켓(Bucket, 저장 공간)을 사용하는 방식입니다.
오픈 어드레싱에는 여러 기법이 있지만, 파이썬은 개방 주소 지정법(Open Addressing with Pertubation)을 사용합니다. 이 방법은 충돌이 발생하면 다음 해시 주소로 이동하는 방식입니다.
예를 들어, 초기 해시 주소가 5이고, 충돌이 발생했다면 해시 함수를 변형하여 다음 주소를 계산합니다. 이 과정을 충돌이 일어나지 않을 때까지 반복합니다.
파이썬은 이차 탐색(Quadratic Probing) 기법을 사용하여 다음 해시 주소를 계산합니다. 이 기법은 충돌이 발생할 때마다 해시 주소를 제곱 수열로 이동시킵니다. 예를 들어 초기 주소가 5이고 충돌이 발생하면, 다음 주소는 6, 그 다음은 9, 16, 25, ... 순으로 탐색합니다.
이러한 기법을 사용하면 해시 충돌 확률을 낮출 수 있습니다. 하지만 여전히 최악의 경우 O(n)의 시간 복잡도가 발생할 수 있습니다. 예를 들어 모든 키가 동일한 해시값을 가지면 딕셔너리 전체를 순회해야 합니다.
딕셔너리 최적화 방법
해시 충돌로 인한 성능 문제를 방지하기 위해서는 다음과 같은 방법을 고려해볼 수 있습니다.
- 키로 문자열을 사용하기 숫자나 불변 자료구조를 키로 사용하는 것보다 문자열 키를 사용하는 것이 더 나은 성능을 보입니다. 파이썬의 문자열 해싱 알고리즘이 효율적이기 때문입니다.
- 좋은 키 설계하기 키의 품질이 중요합니다. 키 값들이 잘 분산되어 있으면 해시 충돌 가능성이 낮아집니다. 키로 사용할 값을 잘 선택하는 것이 중요합니다.
- 해시 함수 변경하기 파이썬에서는 __hash__ 메서드를 재정의하여 사용자 정의 해시 함수를 사용할 수 있습니다. 이를 통해 더 나은 해시 분산을 얻을 수 있습니다.
- 딕셔너리 크기 제한하기 딕셔너리의 크기가 너무 커지면 성능이 저하될 수 있습니다. 이 경우 딕셔너리를 여러 개로 나누어 사용하는 것이 좋습니다.
- 다른 자료구조 사용하기 딕셔너리가 효율적이지 않은 상황이라면 다른 자료구조를 사용하는 것이 좋습니다. 예를 들어 정렬된 리스트에서는 이진 탐색을 사용하여 O(log n)의 시간 복잡도로 검색할 수 있습니다.
딕셔너리는 파이썬에서 매우 유용한 자료구조이지만, 사용 목적과 데이터 특성에 맞게 적절히 활용하는 것이 중요합니다. 이를 통해 프로그램의 효율성과 성능을 극대화할 수 있습니다.
실전 예제: 딕셔너리 활용하기
딕셔너리의 활용 예시를 통해 좀 더 실용적인 이해를 해보겠습니다.
예제 1) 단어 빈도수 계산
문장에 포함된 각 단어의 빈도수를 계산하는 프로그램을 만들어봅시다.
text = "Apple Banana Fruit Apple Banana Juice Juice"
words = text.split() # 단어 리스트 생성
word_counts = {} # 빈 딕셔너리 생성
for word in words:
if word in word_counts:
word_counts[word] += 1 # 이미 있는 단어라면 빈도수 증가
else:
word_counts[word] = 1 # 새로운 단어라면 빈도수 1로 초기화
print(word_counts)
# 출력: {'Apple': 2, 'Banana': 2, 'Fruit': 1, 'Juice': 2}
이 예제에서는 문장을 단어 리스트로 분리한 후, 딕셔너리에 각 단어의 빈도수를 저장하고 있습니다. 이런 식으로 딕셔너리를 사용하면 자료 구조에 대한 복잡한 별도의 처리 없이 간단하게 단어 빈도수를 계산할 수 있습니다.
예제 2) 학생 성적 관리
딕셔너리를 이용하면 학생 이름과 성적을 효과적으로 관리할 수 있습니다.
scores = {
'철수': {'국어': 90, '영어': 85, '수학': 92},
'영희': {'국어': 78, '영어': 92, '수학': 88},
'민수': {'국어': 82, '영어': 68, '수학': 75}
}
# 전체 학생 성적 출력
for 학생, 과목별점수 in scores.items():
총점 = sum(과목별점수.values())
print(f"{학생}의 총점은 {총점}점 입니다.")
# 영희 학생 영어 점수 출력
print(f"영희의 영어 점수: {scores['영희']['영어']}")
이렇게 딕셔너리를 중첩해서 사용하면 복잡한 데이터 구조를 간결하게 표현할 수 있습니다. 또한 학생 이름을 키로 사용하면 개별 학생에 대한 정보를 쉽게 관리할 수 있습니다.
예제 3) 딕셔너리 최적화
앞서 설명한 딕셔너리 최적화 기법 중 사용자 정의 해시 함수를 활용해보겠습니다. 파이썬에서는 __hash__ 메서드를 재정의하여 객체의 해시값을 직접 지정할 수 있습니다.
class 학생:
def __init__(self, 이름, 학번):
self.이름 = 이름
self.학번 = 학번
def __str__(self):
return f"{self.이름}({self.학번})"
def __hash__(self):
return hash((self.이름, self.학번))
학생1 = 학생("철수", 20201234)
학생2 = 학생("영희", 20225678)
학생들 = {
학생1: 100,
학생2: 90
}
print(학생들[학생1]) # 100
print(학생들[학생2]) # 90
위 예제에서는 학생 클래스를 정의하고 __hash__ 메서드를 재정의했습니다. 이름과 학번을 해시 키로 사용하면 충돌 가능성이 낮아져 딕셔너리 성능이 향상됩니다.
사용자 정의 해시 함수를 적절히 활용하면 해시 충돌 확률을 최소화할 수 있습니다. 하지만 해시 함수 설계는 까다로울 수 있으므로, 가능하다면 문자열 키를 사용하는 것이 더 좋습니다.
참고 자료
- Luciano Ramalho (2015), Fluent Python, O'Reilly Media. [제7장 함수를 첫 번째 객체로 대하기: 일급 함수]
- 조상현 (2023), 혼자 공부하는 파이썬, 한빛미디어.
- 백기선 (2018), 혼자 공부하는 파이썬, 한빛아카데미.
- Diogo Coelho, HashMap Internal Implementation in Python https://stackabuse.com/hash-maps-and-hash-tables-in-python/
한 고대 문서 이야기
여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오
gospel79.tistory.com
유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법
올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을
stock79.tistory.com
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
'IT > 파이썬 기초 완전 정복' 카테고리의 다른 글
파이썬 문자열 처리 기초 (14) (1) | 2024.04.20 |
---|---|
파이썬 집합 자료형 사용하기 (13) (1) | 2024.04.20 |
파이썬 리스트 자료구조 다루기 (10) (1) | 2024.04.20 |
파이썬 튜플 활용법 배우기 (11) (1) | 2024.04.20 |
파이썬 예외 처리(try, except) 방법 (9) (1) | 2024.04.20 |
댓글