본문 바로가기
IT/파이썬 기초 완전 정복

파이썬 딕셔너리 자료구조 익히기 (12)

by 지식 발전소 2024. 4. 20.
728x90
반응형

 

딕셔너리란 무엇인가?

 

 

안녕하세요. 오늘은 파이썬 프로그래밍에서 가장 많이 사용되는 자료구조 중 하나인 딕셔너리(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)의 시간 복잡도가 발생할 수 있습니다. 예를 들어 모든 키가 동일한 해시값을 가지면 딕셔너리 전체를 순회해야 합니다.

딕셔너리 최적화 방법

해시 충돌로 인한 성능 문제를 방지하기 위해서는 다음과 같은 방법을 고려해볼 수 있습니다.

  1. 키로 문자열을 사용하기 숫자나 불변 자료구조를 키로 사용하는 것보다 문자열 키를 사용하는 것이 더 나은 성능을 보입니다. 파이썬의 문자열 해싱 알고리즘이 효율적이기 때문입니다.
  2. 좋은 키 설계하기 키의 품질이 중요합니다. 키 값들이 잘 분산되어 있으면 해시 충돌 가능성이 낮아집니다. 키로 사용할 값을 잘 선택하는 것이 중요합니다.
  3. 해시 함수 변경하기 파이썬에서는 __hash__ 메서드를 재정의하여 사용자 정의 해시 함수를 사용할 수 있습니다. 이를 통해 더 나은 해시 분산을 얻을 수 있습니다.
  4. 딕셔너리 크기 제한하기 딕셔너리의 크기가 너무 커지면 성능이 저하될 수 있습니다. 이 경우 딕셔너리를 여러 개로 나누어 사용하는 것이 좋습니다.
  5. 다른 자료구조 사용하기 딕셔너리가 효율적이지 않은 상황이라면 다른 자료구조를 사용하는 것이 좋습니다. 예를 들어 정렬된 리스트에서는 이진 탐색을 사용하여 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__ 메서드를 재정의했습니다. 이름과 학번을 해시 키로 사용하면 충돌 가능성이 낮아져 딕셔너리 성능이 향상됩니다.

사용자 정의 해시 함수를 적절히 활용하면 해시 충돌 확률을 최소화할 수 있습니다. 하지만 해시 함수 설계는 까다로울 수 있으므로, 가능하다면 문자열 키를 사용하는 것이 더 좋습니다.

참고 자료

  1. Luciano Ramalho (2015), Fluent Python, O'Reilly Media. [제7장 함수를 첫 번째 객체로 대하기: 일급 함수]
  2. 조상현 (2023), 혼자 공부하는 파이썬, 한빛미디어.
  3. 백기선 (2018), 혼자 공부하는 파이썬, 한빛아카데미.
  4. 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

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90
반응형

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

댓글