[10/20] 딕셔너리
글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist 를 바탕으로 작성된 글임을 알려드립니다.
이 포스트는 Dictionaries를 한글로 요약 정리한 글입니다.
딕셔너리 (Dictionaries)
이번 장에서는 줄리아에 내장된 다른 데이터 타입인 딕셔너리에 대해서 알아볼 것이다.
딕셔너리는 매핑(mapping)이다
딕셔너리는 배열 같지만 좀 더 일반적이다. 배열에서는 인덱스가 무조건 정수여야 하지만, 딕셔너리에서는 어떤 타입도 인덱스로 사용될 수 있다.
딕셔너리는 키(key)라고 불리는 인덱스들의 모음과 값들의 모음을 포함한다. 각각의 키는 하나의 값과 연결되어 있다. 키와 값의 연결을 키-값 페어(key-value pair) 또는 아이템(items)이라고 부른다.
수학적 용어에서 딕셔너리는 키에서 값으로의 매핑을 나타내므로 각 키가 값에 매핑한다고 말할 수도 있다. (여기서 매핑이란 원하는 데이터를 찾아주는 과정을 말한다) 예를 들어 영어에서 스페인어로 번역해주는 맵인 딕셔너리를 만든다면, 키와 값은 모두 문자열이어야 한다.
Dict()
는 아이템 없이 빈 딕셔너리를 만든다. Dict
는 내장 함수 이름이기 때문에, 변수를 만들 때는 이 이름을 사용하면 안된다.
1 |
|
딕셔너리의 종류는 중괄호로 묶어서 표현된다. 위의 예시에서는 키와 값의 데이터 타입이 모두 Any
이다.
위의 딕셔너리는 비어있다. 딕셔너리에 아이템을 추가하기 위해서는 대괄호를 사용하면 된다.
1 |
|
위 코드는 "one"이라는 키에 "uno"라는 값을 매핑하여 아이템을 생성한다. 이후 딕셔너리를 출력해보면 키-값 사이에 =>
화살표가 나타남을 볼 수 있다.
1 |
|
위의 출력된 형태는 입력할 때도 사용할 수 있다. 예를 들어, 3개의 아이템을 딕셔너리에 추가해보자.
1 |
|
모든 키와 값은 문자열이다. 따라서 Dict{String,String}
으로 생성된다.
WARNING 키-값 페어의 순서가 같지 않을 수 있다. 즉, 컴퓨터에 위의 동일한 예제를 입력해도 순서는 다르게 나타날 수 있다.
하지만 딕셔너리의 요소들은 정수 인덱스로 저장되지 않기 때문에 순서는 문제가 되지 않는다. 대신에 키를 통해서 값을 찾을 수 있다.
1 |
|
키 "two"는 "dos"와 매핑되어 있기 때문에 항상 같은 값을 제공한다.
만약 입력한 키가 딕셔너리에 없다면, 아래와 같은 오류를 만날 수 있다.
1 |
|
lenth()
는 딕셔너리에서도 작동한다. 이 함수는 키-값 페어의 개수를 반환한다.
1 |
|
keys()
는 딕셔너리의 키들(keys)만 보여준다.
1 |
|
키들의 모음을 변수로 만들었으니, 이제는 ∈
연산자를 사용하여 해당 키가 존재하는지 여부를 살펴볼 수 있다.
1 |
|
딕셔너리에 있는 값만을 보기 위해서는 값들의 모음을 반환하는 values()
를 사용하면 된다. 그리고 ∈
연산자를 사용하여 해당 값이 존재하는지 확인할 수 있다.
1 |
|
∈
연산자는 배열과 딕셔너리에서 다른 알고리즘을 사용한다. 배열에서는 검색파트에서와 같이 배열의 요소들을 순서대로 검색한다. 따라서 배열이 길수록, 검색시간은 더 길어질 수밖에 없다.
하지만 딕셔너리에서는 줄리아가 해시테이블(hash table)이라는 알고리즘을 사용한다. 따라서 딕셔너리에서 ∈
연산자는 어떤 아이템을 요구하던 호출하는 시간이 동일하다.
딕셔너리로 카운팅하기
만약 문자가 주어지고 각 문자가 몇 번 나타내는지를 세고 싶다고 가정해보자. 그렇다면 해결할 수 있는 방법으로는 몇 가지가 있다.
- 알파벳마다 하나씩 26개의 변수를 만들고, 각 문자에 대해 조건문을 사용하고 문자열을 순회하여 카운터를 증가하기
- 26개의 요소로 배열을 만든 다음 내장 함수
Int()
를 사용하여 각 문자열을 숫자로 변환한 후, 인덱스로 사용하여 카운터를 증가하기 - 문자를 키로, 카운터를 해당 값으로 사용하여 딕셔너리를 만들고 문자가 나올 때마다 값을 증가하기. 딕셔너리에 없는 문자가 나오면 키로 저장하게 설정하면 좋다.
위의 세 가지의 방법들은 똑같은 결과를 보여주지만, 구현하는 방법은 모두 다르다.
구현(implementation)은 계산을 수행하는 방법이다. 몇몇의 구현은 다른 것들보다 더 나은 경우도 있다. 예를 들어 딕셔너리 구현의 장점은 문자열에 어떤 문자가 나타나는지 미리 알 필요가 없으며, 나타나는 문자를 위한 공간만 만들면 된다는 점이다.
아래의 코드가 그렇다.
1 |
|
함수 이름은 histogram()
으로, 빈도 모음에 대한 통계 용어이다. 함수의 첫 번째 줄은 빈 딕셔너리를 만든다. for
루프는 문자열을 통과하며, 루프는 문자 c
가 딕셔너리에 없다면 키 c
와 초기 값 1
을 새 항목으로 만든다. 만약 c
가 이미 딕셔너리에 있다면 d[c]
를 증가시킨다.
위 함수를 실행한 결과는 다음과 같다.
1 |
|
히스토그램은 위와 같이 문자가 사용된 개수를 정확하게 측정하였다.
딕셔너리는 키와 기본값을 사용하는 get()
가 있다. 키가 딕셔너리에 나타나면 get()
은 연결된 값을 보여주며, 만약 연결된 값이 없다면 기본값을 보여준다.
1 |
|
루핑과 딕셔너리
for
문 안에서 딕셔너리 키들을 순회할 수 있다. 예를 들어서 printhist()
는 각 키에 연결된 값들을 출력한다.
1 |
|
출력 결과는 아래와 같다.
1 |
|
다시 말하지만 키는 특정한 순서를 가지고 있지 않다. 키를 순서대로 순회하기 위해서는 sort()
와 collect()
를 결합해야 한다. collect()
는 키들을 문자로 변경하고, sort()
는 문자들을 순서대로 정렬한다.
1 |
|
역방향 조회
d
라는 딕셔너리와 k
라는 키가 주어졌일 때, 해당된 값을 찾는 방법은 v=d[k]
이다. 이런 작업을 조회(lookup)라고 부른다.
하지만 값을 가진 상태에서 키를 찾고 싶다면 어떻게 해야할까? 이 상황에서는 두 가지의 문제가 있다. 첫 번째 문제는 동일한 값을 매핑한 키가 두 개 이상일 수도 있다는 점이다. 프로그램에 따라 하나를 선택하거나 해당되는 모든 키가 포함된 배열을 만들어야 할 수도 있다. 두 번째는 역방향 조회를 수행하는 간단한 문법은 없다는 점이다. 따라서 우리가 직접 검색해야 한다.
아래의 함수는 값을 받아서 키를 돌려준다.
1 |
|
위 함수는 검색 패턴의 또 다른 예이지만 이전에는 볼 수 없었던 error()
를 보여준다. error()
는 정상적인 제어의 흐름을 방해하는 ErrorException
을 생성하는데 사용된다. 이번 예시에서는 키가 존재하지 않음을 나타내는 "LookupError"
메시지가 표시된다.
루프의 마지막까지 작동된다면 값 v
는 딕셔너리 안에 없는 것이기 때문에 오류 메시지를 전송하는 것이다.
역방향 조회를 성공한 예시는 아래와 같다.
1 |
|
실패한 예시도 확인해보자.
1 |
|
예외를 알려주는 형태는 줄리아가 예외를 알려주는 방식과 동일하다. 즉, 스택트레이스와 오류 메시지를 인쇄하는 것이다.
줄리아는 역방향 조회를 수행하는 최적화된 방법인 findall(isequal(3), h)
을 제공한다.
WARNING 역방향 조회는 순방향 조회보다 훨씬 느리다. 만약 이 방법을 자주 실행하거나 딕셔너리가 크다면, 프로그램이 성능이 저하된다.
딕셔너리와 배열
배열은 딕셔너리에서 값들로 나타낼 수 있다. 예를 들어, 알파벳(키)이 몇 번 쓰였는지 나타낸 빈도(값)로 매핑되는 딕셔너리가 제공된 경우 이를 반대로도 매핑할 수 있다. 즉, 빈도에서 문자를 매핑한 딕셔너리를 만드는 것이다. 사용 빈도가 동일한 알파벳이 여러 개 있을 수 있으므로 반전된 딕셔너리의 각 값은 문자 배열이어야 한다.
아래의 함수는 딕셔너리를 뒤집어준다.
1 |
|
루프가 실행될 때마다 keys
는 d
에서 key
를 얻고, val
은 그 key
와 매핑된 값을 얻는다. 만약에 val
이 inverse
에 없다면 아직 val
과 겹치는 키가 없다는 의미이기 때문에 새 항목을 만들고 초기화한다. 그렇지 않다면 이미 동일한 키가 있는 것이기에 해당 키를 배열에 추가해준다.
예시는 아래와 같다.
1 |
|

위의 상태 다이어그램은 hist
와 inverse
를 보여준다. 딕셔너리는 키-값 페어가 한 박스 안에서 보여진다. 따라서 만약 값이 정수이거나 소수, 문자열이라면 박스 안쪽에 그려야 한다. 하지만 배열의 경우에는 모두 분리된 박스로 그려야 한다. 위의 그림에서도 딕셔너리와 배열을 구분할 수 있다.
Note 앞서 해시테이블을 사용하여 딕셔너리를 구현했으며, 키를 해시할 수 있어야 함을 언급했다. 해시는 값을 취하고 정수를 반환하는 함수이다. 딕셔너리는 이런 해시 함수의 원리를 사용하여 키-값 쌍을 저장하고 조회한다.
메모
우리는 5장 연습해보기에서 피보나치 함수에 대해 살펴보았다. 위의 피보나치 함수는 인수가 클수록 함수를 실행하는 데 시간이 오래 걸린다는 걸 알 수 있다. 더욱이 런타임도 빠르게 증가한다.
왜 그런지 이해하기 위해 피보나치의 n값이 4일 때의 콜 그래프(Call graph)를 확인해보자.

콜 그래프는 함수가 호출하는 흐름을 화살표로 보여준다. 그래프 맨 위에서 n = 4
인 피보나치가 n = 3
, n = 2
인 피보나치를 호출한다. 그 후 n = 3
인 피보나치가 n = 2
와 n = 1
인 피보나치를 호출한다. 이런 흐름이 계속된다.
결국 fibonacci(0)
과 fibonacci(1)
가 몇 번 호출되는지 계산한다. 이것은 문제에 대한 비효율적인 해결방법이며, 인수인 n
이 커질수록 상황은 더욱 악화된다.
한 가지 해결책은 딕셔너리에 저장하여 이미 계산된 값을 추적하는 것이다. 나중에 사용하기 위해 이전에 계산된 값을 메모라고 한다. 피보나치의 메모가 추가된 버전은 다음과 같다.
1 |
|
known
은 이미 사용된 피보나치 숫자들을 저장하는 딕셔너리이다. 두 개의 항목으로 시작하며, 0
은 0
에 매핑되고 1
은 1
에 매핑된다.
fibonacci()
를 호출할 때마다 known
을 확인한다. 만약 해당 결과가 이미 known
에 있다면 즉시 결과를 출력한다. 결과가 없다면 새로운 값을 계산하여 딕셔너리에 추가한 후 결과를 출력한다.
메모가 추가된 fibonacci()
와 원래 fibonacci()
를 비교해보면, 무엇이 더 빠른지 확인할 수 있다.
글로벌 변수들
이전의 예시에서 known
은 함수 밖에서 만들어졌기 때문에 Main
이라는 특별한 프레임에 속한다. Main
의 변수는 가끔 글로벌(global)이라고 불리는데, 그 이유는 그들이 어떤 함수에서 엑세스 할 수 있기 때문이다. 함수 작동이 멈추면 사라지는 로컬(local) 변수들과 달리, 글로벌 변수는 계속 유지된다.
플래그(flags)에 글로벌 변수를 사용하는 것은 일반적이다. 즉, 조건이 참인지 여부를 나타내는 불 변수이다. 예를 들어, 몇몇의 프로그램은 출력 세부 사항의 수준을 제어하기 위해서 verbose
라는 플래그를 사용한다.
1 |
|
글로벌 변수를 재할당하려 하면 놀랄 수 있다. 다음 예제를 확인해보자.
1 |
|
위의 함수를 작동해도 been_called
의 값은 변하지 않는다. 그 이유는 example2()
가 been_called
라는 새로운 로컬 변수를 작성하기 때문이다. 함수가 끝나면 로컬 변수는 사라지고 글로벌 변수에는 영향을 미치지 않는다.
글로벌 변수를 함수 안에서 재할당하려면 먼저 변수 앞에 global
을 써서 선언해야 한다.
1 |
|
글로벌 명령문은 인터프리터에게 이렇게 말하는 것과 같다. "이 함수에서 사용되는 been_called
는 글로벌 변수를 말하는 것이다. 그러니 로컬 변수를 만들지 말아라"
여기 글로벌 변수를 업데이트하려고 시도하는 예시가 있다.
1 |
|
위의 예시를 실행하면 아래와 같은 오류 메시지가 출력된다.
1 |
|
줄리아는 count
를 로컬 변수로 가정했기 때문에 문제가 발생한 것이다. 따라서 해결책은 count
를 글로벌 변수로 선언하는 것이다.
1 |
|
만약 글로벌 함수가 변경 가능한 값으로 제공되면, global
선언 없이 수정할 수 있다.
1 |
|
글로벌 배열이나 딕셔너리의 요소들도 추가하거나 삭제, 대체 등을 할 수 있지만, 변수를 재할당하는 것은 global
을 선언해야 한다.
1 |
|
성능상의 이유로 글로벌 변수는 const
를 선언해야 한다. 더이상 변수를 재할당 할 수는 없지만 변경 가능한 값들 한해서는 값을 수정할 수 있다.
1 |
|
WARNING 글로벌 변수는 유용하지만, 글로벌 변수가 너무 많고 자주 수정하면 프로그램을 디버그하는데 힘들고 성능도 저하될 수 있다.
디버깅
큰 데이터셋으로 작업할 때, 직접 다 출력하여 확인하는 방식으로는 디버그하기 어려울 수 있다. 다음은 큰 데이터셋을 디버그하기 위한 몇 가지의 제안이다.
입력을 축소해라 가능하다면, 데이터셋의 크기를 줄여라. 예를 들어 프로그램이 텍스트 파일을 읽은 경우 처음 10행으로 시작하는 등의 가장 작은 예를 사용하여 오류를 찾을 수 있다. 파일 자체를 편집하는 것보다 프로그램을 수정하여 첫 번째 'n'라인만 읽을 수 있도록 수정하는 것이 낫다.
요약 및 데이터 타입을 확인하라 전체 테이터셋을 인쇄하고 확인하는 대신 데이터의 요약을 인쇄해라. (예로 딕셔너리에 있는 아이템 수나 배열의 숫자 등) 런타임 오류의 일반적인 원인은 올바르지 못한 데이터 타입의 값 때문이다. 이런 종류의 오류를 디버깅하기 위해선 값 데이터 타입을 출력해보는 것만으로도 충분하다.
자가 점검을 작성하라 때로는 오류를 자동으로 확인하는 코드를 작성할 수 있다. 예를 들어 숫자 배열의 평균을 계산하는 경우 결과가 배열에서 가장 큰 요소보다 크지 않거나 가장 작은 요소보다 작지 않은지 확인할 수 있다. 이것을 분별 검사(sanity check)라고 한다. 다른 종류의 검사는 서로 다은 두 가지 계산 결과를 비교하여 일치하는지 확인한다. 이것을 일관성 검사(consistency check)라고 한다.
출력을 형식화해라 디버깅 출력 형식을 지정하면 오류를 더 쉽게 발견할 수 있다. 출력 형식을 지정하는 것을 스캐폴딩이라고 하며, 이에 대한 자세한 설명은 해당 페이지 디버깅 파트에 있다. 다시 말하지만 스캐폴딩을 구축하는 데 소요되는 시간은 디버깅에 낭비되는 시간을 줄일 수 있다.