매핑 씨는 괴짜 프로그래머 시퀀스 씨의 집에서 화장실을 찾다가 바지에 실례를 할 뻔 했다. 글쎄, 여러 방문에 0, 1, 2, 3 식의 번호만 붙어 있는게 아닌가? 어느 문에 화장실이 있을지 몰라 모든 문을 다 열어봐야 했다.

시퀀스는 데이터를 순서대로 관리하기에는 좋지만 어떤 위치에 내포된 의미가 무엇인지 알기는 어렵다. 현실에서도 사람들은 0, 1, 2, 3 이 아니라 안방, 서재, 화장실, 보일러실과 같은 간판을 사용하고, 고층 건물에서는 번호로 방을 관리하더라도 0, 1, 2, 3의 단순 나열이 아니라 층이나 구획에 따라 101, 201, A1, B2 처럼 의미 있는 번호를 사용한다. 컬렉션에서도 좀 더 의미 있는 번호나 이름을 사용하면 편리하지 않을까?

5.3.1 키를 이용하는 컬렉션

그림 5-6 메뉴판

그림 5-6 메뉴판

매핑(mapping)은 키(key)와 값(value)의 쌍을 이용해 데이터를 관리하는 구조다. 키는 저장된 데이터를 식별하기 위한 번호 또는 이름이며, 값은 각 키에 연결되어 저장된 데이터다. 시퀀스가 데이터를 단순히 순서에 의해 관리하는 데 비해 매핑은 키를 이용해 데이터를 저장하고 꺼낸다. 키만 알면 원하는 데이터를 바로 찾을 수 있다.

매핑의 예는 일상에서도 발견할 수 있다.

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

위의 두 예는 메뉴판과 단어장에서 간추린 것이다. 메뉴판은 음료 이름(키)과 가격(값)을 연결한 정보 체계다. 아메리카노에는 2500이, 카페 라떼에는 3000이 연결되어 있다. 단어장도 마찬가지로 단어(키)와 의미(값)가 연결된 일종의 매핑으로 볼 수 있다. 정보를 이렇게 연결해 둠으로써 키를 통해 값을 쉽게 파악할 수 있는 것이다.

매핑의 특징을 요약해 보자.

  • 키와 값을 쌍으로 연결해, 모은 것이다.
  • 순서 대신 키로 데이터를 관리한다.
  • 키를 이용해 데이터를 가리킬 수 있다.

5.3.2 사전

파이썬이 제공하는 매핑 데이터 유형으로는 사전(dictionary)과 순서 있는 사전이 있다. 사전은 파이썬의 내장 기능이지만 순서 있는 사전은 내장 기능이 아니며 collections 모듈을 통해 지원된다. 이 책에서는 사전에 대해서만 설명한다.

매핑의 이름과 종류

프로그래밍 언어마다 제공하는 매핑 컬렉션의 이름이 조금씩 다르다. 매핑은 해시 알고리즘을 이용해 구현하는 것이 일반적이어서 ‘해시 맵(hash map)’ 또는 ‘해시 태이블(hash table)’이라는 이름으로 많이 불린다. 해시 맵은 데이터를 순서 없이 저장하므로 ‘순서 없는 맵(unordered map)’이라고도 불린다. 파이썬의 사전도 해시 알고리즘을 사용하는 방식인데, 이름이 좀 더 직관적이다.

해시 맵은 원소의 순서를 관리하지 않으며, 키를 정렬하는 것도 불가능하다. 매핑 컬렉션 중에는 이 점을 보완하여 원소의 순서를 관리할 수 있도록 한 ‘순서 있는 맵(ordered map)’과 키를 항상 정렬해 관리하는 ‘정렬된 맵(sorted map)’ 등도 있다.

사전 표현하기

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

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

사전을 정의할 때, 키가 중복되어서는 안 된다. 하나의 키에는 하나의 값만 포함될 수 있다.

이 양식에 따라 메뉴판을 사전으로 표현하면 다음과 같다.

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

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

만일 내용이 길지 않다면 다음과 같이 하나의 행으로 표현해도 된다.

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

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

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

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

{}  # 빈 사전

키로 사용할 수 있는 데이터

사전의 키로는 대개 문자열이 사용된다. 하지만 정수, 튜플 등 다른 데이터도 키로 사용할 수 있다. 그러나 리스트, 사전 같은 가변 데이터는 키로 사용할 수 없으며, 가변 데이터를 담은 튜플도 키로 사용할 수 없다.

코드 5-41 다양한 키

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

연습문제

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

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

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

5.3.3 매핑 연산과 메서드

매핑에서 사용할 수 있는 연산과 메서드를 알아보자. 사전이 파이썬의 유일한 기본 매핑 데이터이기 때문에 IDLE 대화식 셸에서 사전을 정의하고 사용해보는 방식으로 진행할 것이다. 계속 강조하고 있듯, 대화식 셸에 직접 따라해보는 것이 학습에 큰 도움이 된다.

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

코드 5-42 사전 정의하기

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

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

키 검사

사전에 어떤 키가 있는지 확인할 때는 in을, 없음을 검사하려면 not in을 사용한다. 시퀀스에서는 in이 어떤 원소가 있는지를 검사하지만, 사전에서는 값이 아니라 키를 검사한다는 점이 다르다. 헷갈릴 수 있으니 유의하자.

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

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

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

키-값 쌍의 개수 세기

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

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

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

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

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

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

코드 5-45 키로 값 구하기

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

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

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

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

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

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

키 추가 / 값 수정

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

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

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

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

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

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

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

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

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

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

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

키 삭제하기

어떤 키를 (그리고 연결된 값도) 삭제하고 싶다면 del 사전[키] 명령을 사용한다.

코드 5-50 키 하나 삭제하기

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

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

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

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

키 시퀀스와 값 시퀀스

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

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

>>> price_list = [2500, 3000, 3500]
>>> drink_list = ['아메리카노', '카페 라떼', '딸기 주스']

하나의 시퀀스에서는 가격만이 표현된다.(price_list) 이 시퀀스에서는 어떤 음료가 있는지와 어느 음료가 얼마인지 같은 정보는 알 수 없다. 따라서 음료의 정보를 별도의 시퀀스(drink_list)로 표현해 두었다. 시퀀스의 각 원소는 위치(인덱스)를 매개로 상대 시퀀스의 원소와 연관지을 수 있다. 이로써 각 음료의 가격을 알 수 있다. 예를 들면, 첫번째 음료는 아메리카노이고, 첫번째 가격은 2500이다. 따라서 아메리카노는 2500원이다.

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

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

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

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

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

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

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

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

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

zip() 함수의 동작

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

연습문제

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

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

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

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

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

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

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

5.3.4 사전으로 연락처 묶기

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

사전을 이용해 묶기

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

사전을 이용하면 두 변수를 하나로 묶을 수 있다.

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

{
    'name': '홍길동',
    'phone': '01234567890',
}

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

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

{
    'name': '홍길동',
    'phone': 01234567890',
    'address': '율도국',
    'email': 'dong@gil.hong',
}

묶은 데이터를 리스트에 담기

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

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

>>> contact_list = []  # 연락처를 담는 리스트
>>> contact_list.append({'name': '홍길동', 'phone': '01234567890'})  # 새 연락처 추가
>>> contact_list.append({'name': '임꺽정', 'phone': '01234567891'})  # 새 연락처 추가
>>> contact_list[0]    # 첫 번째 연락처 확인
{'name': '홍길동', 'phone': '01234567890'}

>>> contact_list[0]['name']  # 첫 번째 연락처의 이름 확인
'홍길동'

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

왜 사전으로 묶어야 할까?

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

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

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

# 리스트: 첫번째 원소는 이름, 두번째 원소는 전화번호
['홍길동', '01234567890']

# 튜플: 첫번째 원소는 이름, 두번째 원소는 전화번호
('임꺽정', '01234567891')

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

문자열로 데이터 표현하기

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

# 방식 1: 위치로 구별
# [0:3] = 이름 / [3:14] = 전화번호
'홍길동01234567890'

# 방식 2: 구분자(,)로 구별
# split(',') 메서드로 나눈 후, 첫번째는 이름, 두번째는 전화번호
'홍길동,01234567890'

# 방식 3: JSON
# JSON 표기법에 따라 데이터를 표현
'{"name":"홍길동","phone":"01234567890"}'

문자열로 복잡한 데이터를 표현하려면 규칙과 분석기(parser)를 만들어야 하므로 기본 컬렉션을 사용하는 것보다 불편하다. 하지만 텍스트 데이터 자체는 특정 프로그래밍 언어에 종속되지 않는다(각 프로그래밍 언어로 해석기를 만들면 되므로)는 장점이 있어 외부 프로그램과 통신할 때나 데이터를 파일로 저장할 때 종종 사용된다.

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