어떤 데이터는 단순히 나열해 두기만 하면 활용하기가 어렵다. 예를 들어, 카페에서 메뉴판을 만들 때 2500 원, 3000 원, 3500 원, ...과 같이 가격을 나열해 두기만 한다면 어떨까? “1번 음료는 2500원이다”하는 정보밖에 알 수 없을 것이다. 대체 1번 음료가 뭐길래? 아메리카노: 2500 원, 카페 라테: 3000원, 딸기 주스: 3500 원, ...과 같이 음료의 이름과 가격 데이터가 짝지어져야 메뉴판으로써 역할을 할 수 있지 않을까? 이름 역할을 하는 데이터와 값 역할을 하는 데이터를 짝지어 관리하는 방법을 알아보자.

5.3.1 키와 값을 짝지은 데이터 구조

매핑(mapping)키(key) 역할을 하는 데이터와 값(value) 역할을 하는 데이터를 하나씩 짝지어 저장하는 데이터 구조다. 키는 저장된 데이터를 구별하고 가리키는 데 쓰이고, 값은 그 키와 연결되어 저장된 데이터가 된다. 시퀀스와 비교할 때 가장 큰 차이는 저장된 데이터를 가리킬 때 순서가 아니라 키를 이용한다는 점이다. 데이터를 저장할 때 순서보다 좀 더 의미 있는 식별 방법이 필요하다면 매핑을 사용하는 것이 좋다.

그림 5-6 메뉴판

그림 5-6 메뉴판

일상 생활에서도 매핑 데이터 구조를 쉽게 찾아볼 수 있다.

  • 아메리카노: 2500, 카페 라테: 3000, 딸기 주스: 3500
  • cat: 고양이, hammer: 망치, rainbow: 무지개, book: 책

메뉴판은 음료 이름(키)과 가격(값)을 연결한 정보 체계다. 아메리카노에는 2500이, 카페 라테에는 3000이 연결되어 있다. 단어장도 마찬가지로 단어(키)와 의미(값)가 연결된 일종의 매핑으로 볼 수 있다.

매핑의 특징

  • 키 데이터와 값 데이터를 짝지어 모아 두는 데이터 저장 방식이다.
  • 데이터를 가리킬 때 순서 대신 키를 이용한다.

매핑 컬렉션의 종류

파이썬의 매핑 컬렉션으로는 사전(dict), 기본 값 사전(defaultdict) 등이 있다. 이 컬렉션들은 데이터를 저장하고 표현하는 방식에 약간씩 차이가 있지만, 키와 값을 짝지어 저장한다는 점은 모두 똑같다.

매핑 컬렉션의 이름들

파이썬은 매핑 컬렉션에 직관적으로 이해하기 쉬운 ‘사전(dictionary)’이라는 이름을 붙였다. 나중에 다른 프로그래밍 언어를 배운다면 ‘해시 맵(hash map)’ 또는 ‘해시 태이블(hash table)’이라는 이름을 볼 수도 있다. 매핑을 해시 알고리즘이라는 방식으로 구현하는 경우가 많아서 그런 이름이 많이 사용된다.

5.3.2 사전

사전은 파이썬의 매핑 컬렉션 가운데 가장 대표적이며 활용도가 높다. 이 책에서는 사전에 대해서만 설명할 것이다. 사전만 잘 알아두어도 파이썬 프로그래밍에 어려움이 없으며, 나중에 다른 매핑 컬렉션을 배우더라도 사전과 큰 차이가 없어 금세 배울 수 있을 것이다.

사전 표현하기

사전은 중괄호({, }) 안에 콜론(:)으로 키-값 쌍을 써넣어 표현한다. 키-값 쌍 각각은 콤마(,)로 구분한다.

{
    키1: 값1,
    키2: 값2,
    키3: 값3,
    ...
}

사전을 정의할 때, 키가 중복되어서는 안 된다. 하나의 키에는 하나의 값만 포함될 수 있다. 이 양식에 따라 메뉴판을 사전으로 표현해 보자.

코드 5-39 여러 행으로 사전 표현하기

{
    '아메리카노': 2500,
    '카페 라테': 3000,
    '딸기 주스': 3000,
}

위와 같이 행 하나에 키-값 쌍을 하나씩만 표현해도 되고, 사전의 항목이 많지 않을 때는 아래와 같이 한 행에 모두 기술해도 된다.

코드 5-40 한 행으로 사전 표현하기

{'아메리카노': 2500, '카페 라테': 3000, '딸기 주스': 3500}

내용 없이 중괄호만 열었다 닫으면 빈 사전이 된다. 사전은 변경 가능한 컬렉션이므로 빈 사전을 정의한 후 나중에 필요한 데이터를 추가할 수도 있다.

코드 5-41 빈 사전 표현하기

{}  # 빈 사전

키로 사용할 수 있는 데이터

사전의 키로는 대개 문자열이 사용된다. 하지만 문자열만 써야 하는 것은 아니다. 정수와 튜플 등 불변 데이터를 키로 사용할 수 있다. 그러나 리스트, 사전 등의 가변 데이터는 키로 사용할 수 없다. 또, 튜플에 가변 데이터가 들어있을 때도 키로 쓸 수 없다.

코드 5-42 다양한 데이터 유형을 키로 사용하기

{
    1004: 'value',       # 정수 키
    (1, 2, 3): 'value',  # 튜플 키
    'key': 'value',      # 문자열 키
}

연습문제

연습문제 5-10 식재료별 칼로리 사전

다음 표를 참고해 식재료별 칼로리를 식재료_칼로리 라는 이름의 사전으로 정의해 보아라. 이 사전은 음식의 이름을 키로, 칼로리를 값으로 저장한다.

음식 kcal (100 그램 당)
밀가루 364
피망 20.1
올리브 115
돼지고기 242.1

5.3.3 매핑 연산과 메서드

대화식 셸에서 사전을 요모조모 조작해 보면서 파이썬의 매핑 컬렉션에서 사용할 수 있는 연산과 메서드를 알아보자. 대화식 셸에 직접 따라 입력하면서 이런 저런 실험을 하다 보면 사전과 금방 친해질 것이다.

먼저 연습에 사용할 사전부터 정의하여 변수에 대입해 두자. 사전을 담을 변수의 이름은 자유롭게 지어도 된다. 이 예에서는 사전을 뜻하는 접미사 _dict를 붙여 보았다. 사전을 여러 행에 걸쳐 입력할 때는 들여쓰기 칸 수를 잘 맞추어 오류가 나지 않도록 하자.

코드 5-43 사전 정의하기

>>> word_dict = {
...     'cat': '고양이',
...     'hammer': '망치',
...     'rainbow': '무지개',
...     'book': '책',
... }
>>> word_dict  # 사전 내용 확인
{'cat': '고양이', 'hammer': '망치', 'rainbow': '무지개', 'book': '책'}

입력은 여러 행에 걸쳐 했지만, 대화식 셸이 출력할 때는 한 행에 출력될 것이다. 스타일이 다르더라도 사전의 내용은 동일하다.

키 검사하기

사전에 어떤 키가 있는지 확인할 때는 in을, 없음을 검사하려면 not in을 사용한다.

코드 5-44 사전에 키가 있는지 검사

>>> 'cat' in word_dict      # word_dict에 'cat' 키가 있는지 검사
True

>>> 'dog' not in word_dict  # word_dict에 'dog' 키가 없음을 검사
True

>>> '망치' in word_dict     # ❶ word_dict에 '망치' 키가 있는지 검사
False

❶과 같이 값에 해당되는 데이터를 검사하면 그와 동일한 키가 사전에 있지 않는 한 False로 평가된다. 시퀀스에서는 in 연산이 단순히 어떤 요소가 있는지를 검사하지만, 매핑에서는 값이 아니라 키가 있는지를 검사한다. 헷갈릴 수 있는 부분이니 유의하자.

요소(키-값 쌍)의 개수 세기

사전에 포함된 요소(키-값 쌍)의 개수는 len() 함수로 구할 수 있다. 시퀀스의 길이를 셀 때와 마찬가지다. 사전에서 하나의 키-값 쌍은 요소 하나로 취급한다.

코드 5-45 키-값 쌍의 개수 세기

>>> len({})         # 빈 사전의 키-값 쌍의 개수
0

>>> len(word_dict)  # word_dict의 키-값 쌍의 개수
4

인덱싱 연산으로 키에 해당하는 값 구하기

사전에 담은 데이터는 사전[키] 양식의 인덱싱 연산으로 구할 수 있다. 지정한 키가 사전에 없으면 KeyError 오류가 발생한다.

코드 5-46 키로 값 구하기

>>> word_dict['cat']  # 사전에서 'cat' 키와 연결된 값 구하기
'고양이'

>>> word_dict['dog']  # 오류: 사전에 없는 키
KeyError: 'dog'

KeyError 오류를 피하고 싶으면 get() 메서드를 사용하면 된다. get() 메서드는 키가 있으면 키에 연결된 값을 반환하고, 키가 없으면 None이나 기본값으로 지정한 값을 반환한다.

코드 5-47 get() 메서드로 값 구하기

>>> word_dict.get('cat')          # 키가 있을 경우
'고양이'

>>> word_dict.get('dog')          # 키가 없을 경우 None 반환
>>> word_dict.get('dog', '동물')  # 키가 없을 경우 반환할 기본값 지정
'동물'

요소 추가 / 값 수정하기

사전[키] = 값 표현으로 새로운 키-값 쌍을 추가할 수 있다.

코드 5-48 키-값 쌍 추가하기

>>> word_dict['moon'] = '달'     # 새로운 키-값 쌍 추가
>>> word_dict
{'cat': '고양이', 'hammer': '망치', 'rainbow': '무지개', 'book': '책', 'moon': '달'}    

그런데 대입하려는 키가 사전에 이미 존재한다면 어떻게 될까?

코드 5-49 이미 존재하는 키에 값을 대입하면…

>>> word_dict['cat'] = '야옹이'  # 이미 존재하는 키에 새로운 값을 대입
>>> word_dict                    # 내용을 확인해보면...
{'cat': '야옹이', 'hammer': '망치', 'rainbow': '무지개', 'book': '책', 'moon': '달'}

하나의 키에는 하나의 값만 연결된다. 따라서 이미 존재하는 키에 새로운 값을 대입하면 키에 연결된 값이 교체된다.

다른 사전의 내용으로 덮어쓰기

사전에 다른 사전의 내용을 덮어쓰려면 update() 메서드를 사용한다. 사전에 없는 새로운 키는 추가되고, 동일한 키가 있으면 새로운 값으로 수정된다.

코드 5-50 이미 존재하는 키에 값을 대입하면…

>>> word_dict.update({'star': '별님', 'moon': '달님'})
>>> word_dict
{'cat': '야옹이', 'hammer': '망치', 'rainbow': '무지개', 'book': '책', 'moon': '달님', 'star': '별님'}

요소(키-값 쌍) 삭제하기

어떤 요소(키-값 쌍)을 삭제하고 싶다면 del 사전[키] 명령을 사용한다.

코드 5-51 요소 하나 삭제하기

>>> del word_dict['hammer']   # 'hammer' 키를 삭제
>>> word_dict 
{'cat': '야옹이', 'rainbow': '무지개', 'book': '책', 'moon': '달님', 'star': '별님'}

사전에서 모든 요소를 삭제하려면 clear() 메서드를 사용한다.

코드 5-52 모든 키 삭제하기

>>> word_dict.clear()   # 모든 키를 삭제
>>> word_dict 
{}

연습문제

연습문제 5-11 사전을 이용한 칼로리 계산

연습문제 5-10에서 정의한 식재료별_칼로리 사전을 활용해 칼로리를 계산하는 함수 칼로리()를 정의하라. 이 함수는 음식의 종류와 섭취량을 매개변수에 전달받아 총칼로리를 반환한다. 단, 전달받은 음식이 식재료별_칼로리 사전에 정의되어 있지 않은 경우에는 None을 반환한다. 다음은 이 함수를 대화식 셸에서 실행한 예다.

>>> 칼로리('돼지고기', 500)
1210.5

>>> 칼로리('소고기', 300)
None

연습문제 5-12 칼로리 계산기 확장하기

앞에서 정의한 식재료별_칼로리 사전 또는 칼로리() 함수를 수정하여, 칼로리() 함수가 치즈의 칼로리도 계산할 수 있도록 수정해 보아라. 참고로 치즈의 칼로리는 402.5 kcal / 100g이다.

사전을 수정하는 것과 함수를 수정하는 것 중 어느 방식이 더 편리한가? 그 이유는 무엇인가?

5.3.4 시퀀스와 매핑을 서로 변환하기

매핑은 키와 값을 짝지은 것이다. 키 하나와 값 하나를 짝지으면 매핑의 요소 하나가 된다. 매핑에는 여러 개의 키-값 쌍을 담을 수 있다. 키를 담은 시퀀스와 값을 담은 시퀀스를 서로 짝지어서 매핑을 만들 수 있지 않을까? 반대로, 사전에서 키의 시퀀스와 값의 시퀀스를 구할 수도 있지 않을까?

키 시퀀스와 값 시퀀스로 사전 정의하기

코드 5-40의 메뉴판({'아메리카노': 2500, '카페 라테': 3000, '딸기 주스': 3000})을 사전이 아니라 시퀀스로 표현해 보자.

코드 5-53 메뉴판의 정보를 리스트로 표현했을 때

>>> price_list = [2500, 3000, 3000]                        # ❶
>>> drink_list = ['아메리카노', '카페 라테', '딸기 주스']  # ❷

시퀀스는 키 없이 단순히 값만을 나타낸다. ❶ 가격 리스트 price_list에는 음료의 가격만이 표현되어 있다. 각 요소가 나타내는 가격이 어떤 음료의 것인지를 알 수 없다. 이 정보를 음료와 짝지으려면 ❷ 음료 시퀀스(drink_list)가 함께 필요하다. 이제 위치를 기준으로 두 시퀀스의 요소들을 각각 짝지을 수 있다. 예를 들면, 첫번째 음료는 아메리카노이고, 첫번째 가격은 2500이다. 따라서 아메리카노는 2500원이다.

두 리스트를 따로 두는 것보다는 {'아메리카노': 2500, '카페 라테': 3000, '딸기 주스': 3000} 처럼 함께 짝지어 두는 편이 더 관리하기 좋다. 그런데 두 리스트를 짝지어 놓고 보면 매핑과 형태가 같다. 즉, 매핑은 키 시퀀스와 값 시퀀스를 각 요소끼리 짝지어 연결한 것으로 볼 수도 있다.

따라서 키 시퀀스와 값 시퀀스를 이용해 사전을 정의하는 것도 가능하다. dict(zip(키시퀀스, 값시퀀스)) 명령을 사용하면 된다.

코드 5-54 키 시퀀스와 값 시퀀스로 사전 정의하기

>>> menu_dict = dict(zip(drink_list, price_list))
>>> menu_dict
{'아메리카노': 2500, '카페 라테': 3000, '딸기 주스': 3500}

zip() 함수의 동작

코드 5-54에서 키 시퀀스와 값 시퀀스를 연결해 사전을 만들 때 zip() 함수를 사용했다. zip() 함수는 여러 개의 시퀀스를 입력받아 각 시퀀스의 요소를 순서대로 엮는다. zip() 함수는 7장에서 다시 살펴볼 것이다.

사전에서 키 시퀀스와 값 시퀀스 구하기

반대로 사전에서 키 시퀀스와 값 시퀀스를 얻는 것도 가능하다. 사전의 keys() 메서드, values() 메서드, items() 메서드를 사용하면 된다.

코드 5-55 사전에서 키 시퀀스와 값 시퀀스 얻기

>>> menu_dict.keys()   # 사전의 키 시퀀스
dict_keys(['아메리카노', '카페 라테', '딸기 주스'])

>>> menu_dict.values()   # 사전의 값 시퀀스
dict_values(['아메리카노', '카페 라테', '딸기 주스'])

>>> menu_dict.items()  # 사전의 키-값 쌍 시퀀스
dict_items([('아메리카노', 2500), ('카페 라테', 3000), ('딸기 주스', 3500)])

5.3.5 관련된 정보를 한 덩어리로 묶기

5.1절에서 알아본 데이터 관리 문제를 다시 떠올려 보자. 정해지지 않은 많은 양의 데이터를 관리해야 하는 문제는 이미 리스트를 이용해 해결해 보았다. 하지만 여전히 이름과 전화번호가 한 덩어리로 관리되지 못하고 분리되어 있는 문제를 해결하지는 못했다.

사전을 이용해 정보 묶기

연락처는 이름과 전화번호로 구성되어 있다. 그런데 이 정보를 name, phone이라는 별개의 변수로 관리하면 둘은 서로 연관되지 않은 각각의 정보로 인식되기 쉽다. 이들이 하나의 컬렉션으로 묶여 있다면 연관성이 분명해질 것이다. 이럴 때, 사전을 이용하면 두 변수를 하나로 묶을 수 있다.

코드 5-56 사전으로 연락처 표현하기

{
    'name': '박연오',
    'phone': '01012345678',
}

두 변수에 나뉘어 저장되어 있던 데이터가 하나의 사전 속에 저장되었다. 변수의 이름 name과 phone을 각각 키로 따왔고, 변수에 대입했던 데이터를 키에 연결된 값으로 저장했다. 사전을 이용하면 주소, 이메일 같은 연락처의 구성요소가 늘어나더라도 새로운 변수를 만들 걱정을 하지 않아도 된다. 아래와 같이 새로운 구성요소에 해당하는 키만 늘리면 된다.

코드 5-57 연락처의 구성요소가 늘어나더라도 문제 없다

{
    'name': '박연오',
    'phone': '01012345678',
    'email': 'bakyeono@gmail.com',
    'website': 'https://bakyeono.net',
}

사전으로 묶은 데이터 덩어리들을 리스트에 담기

이제 사전 하나로 연락처 덩어리 하나를 표현하는 방법을 알았다. 그런데 여러 개의 연락처를 저장할 수 있게 하려면 어떻게 해야 좋을까? 몇 개가 될 지 모르는 여러 개의 데이터를 관리할 때는 리스트를 사용하는 것이 좋다. 5.2절에서 연락처를 이름 목록(name_list)과 전화번호 목록(phone_list) 이라는 두 리스트로 관리해 보았다. 이제 연락처 하나를 사전으로 묶었으므로 리스트 두 개 대신, 연락처를 담는 리스트 하나만 있으면 된다.

코드 5-58 연락처의 구성요소가 늘어나더라도 문제 없다

>>> contact_list = []  # 연락처를 담는 리스트
>>> contact_list.append({'name': '박연오', 'phone': '01012345678'})  # 새 연락처 추가
>>> contact_list.append({'name': '이진수', 'phone': '01011001010'})  # 새 연락처 추가
>>> contact_list[0]    # 첫 번째 연락처 확인
{'name': '박연오', 'phone': '01012345678'}

>>> contact_list[0]['name']  # 첫 번째 연락처의 이름 확인
'박연오'

리스트에 담는 데이터가 사전으로 변경되었을 뿐, 데이터를 데이터를 추가하고 조회하는 방법은 동일하다. 이로써 이 장을 시작할 때 알아본 데이터 관리의 세 가지 문제를 모두 해결해 보았다.

꼭 사전으로 묶어야 할까?

데이터를 묶을 때 사전 대신 다른 컬렉션을 사용할 수도 있지 않을까? 사용하기에 따라 리스트, 튜플, 심지어 문자열로도 데이터를 묶을 수 있다. 하지만 묶어 둔 데이터를 식별하기에는 사전이 가장 좋다. 왜 그런지 확인해 보자.

리스트 또는 튜플을 이용해 데이터를 묶어 보자.

코드 5-59 리스트와 튜플로 데이터 묶기

# 리스트: 첫번째 요소는 이름, 두번째 요소는 전화번호
['박연오', '01012345678']

# 튜플: 첫번째 요소는 이름, 두번째 요소는 전화번호
('이진수', '01011001010')

리스트와 튜플로 데이터를 묶는 것은 어렵지 않다. 하지만 각 위치의 데이터가 무엇인지 프로그래머가 별도로 기억해야 한다는 점이 불편하다. 이 프로그램을 처음 보는 사람은 주석이나 별도의 문서가 있어야만 데이터를 파악할 수 있을 것이다.

문자열로 데이터 표현하기

텍스트 데이터는 활용하기에 따라 다양한 정보를 표현할 수 있다. 연락처 정보를 문자열로 표현하는 것도 가능하다. 그런데 복잡한 데이터를 문자열로 표현하기 위해서는 별도의 규칙이 필요하다. 다음은 문자열을 활용하는 몇 가지 방법으로 연락처를 표현해 본 것이다.

# 방식 1: 위치로 구별하기
# [0:3] = 이름 / [3:14] = 전화번호
'박연오01012345678'

# 방식 2: 구분자(,)로 구별하기
# split(',') 메서드로 나눈 후, 첫번째는 이름, 두번째는 전화번호
'박연오,01012345678'

# 방식 3: JSON
# JSON 표기법으로 키와 값 지정하기
'{"name":"박연오","phone":"01012345678"}'

복잡한 데이터를 텍스트만으로 표현하려면 규칙과 분석기(parser)를 만들어야 해서 컬렉션을 사용하는 것보다 불편하다. 하지만 텍스트는 파이썬이 아닌 다른 프로그래밍 언어로 만든 프로그램에서도 해석할 수 있기 때문에 외부 프로그램과 통신할 때나 데이터를 파일로 저장할 때 종종 사용된다.

리스트와 사전을 중첩하면 더 복잡한 구조의 정보를 나타낼 수도 있다. 7장에서 더 자세히 알아 볼 것이다.