티스토리 뷰

openbookproject의 Beginning Python Programming for Aspiring Web Developers 5장 번역  [Copyright Notice]

6. 사전 (dictionaries)

6.1. Dictionaries

사전은 앞에서 배운 리스트, 튜플, 스트링같은 시퀀스와는 좀 다른 복합타입이다. 이것은 파이썬에서 제공하는 빌트인 매핑 타입이다. 즉 키(key)를 값(value)에 매핑시켜주는 자료구조인데, 키는 불변 타입이어야 하고 값은 리스트나 튜플 등 어떤 값이나 가능하다. 이러한 키-값 쌍(key-value pair)은 모든 프로그래밍언어에서 지원되고, 컴퓨터 프로그래밍에서는 매우 자주 사용된다.

예를 들어 영어 단어를 한국어로 번역하는 사전을 만든다고 가정해 보자. 이 사전에 키는 영어 단어 스트링이다. 한 가지 방법은 처음에 빈 사전에서 시작해서 키-값 쌍을 하나씩 추가하는 것이다. 빈 사전은 집합괄호 {}로 표시된다.

>>> eng2kot = {}
>>> type(eng2kor)
<class 'dict'>
>>> eng2kor['one'] = '일'
>>> eng2kor['two'] = '이'
>>> eng2kor['three'] = '삼'

첫 문장은 eng2kor라는 이름의 빈 사전을 생성한다. 나머지 문장은 이 사전에 새로운 키-값 쌍을 추가한다. 이러한 사전의 현재 값을 출력해 볼 수 있다.

>>> print(eng2kor)
{'three': '삼', 'one': '일', 'two': '이'}

사전의 키-값 쌍은 콤마로 구분된다. 각 쌍은 키와 값을 콜론으로 연결하여 표시한다.

print에서 출력되는 키-값 쌍의 순서가 우리가 생각한 것과 다르다. 파이썬은 키-값 쌍을 사전에 저장할 때 복잡한 알고리듬을 적용하여 집어넣는다. 일단 여기서는 이런 순서가 예측할 수 없다고 알고 넘어가자. 그러므로 사전에 들어간 키-값 쌍의 순서를 이용하여 전체를 차례로 찾거나 접근하는 방법은 사용할 수 없고 항상 알고있는 키로 값을 찾아야 한다.

위에서는 사전을 빈 사전에서 출발해서 키-값을 하나씩 추가하는 방법을 살펴보았는데, 사전을 만드는 다른 방법은 키-값 쌍을 차례로 모두 나열하는 다음과 같은 방법이다.

>>> eng2kor = {'one': 일', 'two': '이', 'three': '삼'}

이 때 키-값 쌍을 어떤 순서로 나열하는가는 중요하지 않다. 사전의 값들은 키에 따라 내부적으로 정한 순서로 저장되어 있고 따로 인덱스를 갖지 않는다. 즉 위에서 나열한 키의 순서는 의미가 없다.

이제 사전에 저장되어 있는 키-값을 이용해 키로 대응하는 값을 찾는 방법을 살펴보자.

>>> eng2kor['two']
'이'

이 사전에서 키 'two'는 그에 대응하는 value인 라는 값을 돌려준다

6.2. Dictionary 연산

사전에 적용할 수 있는 연산들이 몇가지 있다. 먼저 del 연산은 사전에서 해당 키-값 쌍을 삭제한다. 예를 들어 다음 사전은 여러 가지 과일과 그 과일의 재고 개수를 가지고 있다.

>>> inventory = {'apples': 430, 'bananas': 312, 'oranges': 525, 'pears': 217}
>>> print(inventory)
{'apples': 430, 'bananas': 312, 'pears': 217, 'oranges': 525}

누군가 배를 모두 사면 사전에서 배 항목을 제거할 수 있다. 만약 배에 대한 정보가 더이상 필요없다면 우리는 다음과 같이 'pears' 항목을 아예 삭제할 수 있다. 

>>> del inventory['pears']
>>> print(inventory)
{'apples': 430, 'bananas': 312, 'oranges': 525}

그런데 만약 추가로 배가 입고될 거라면 우리는 키 'pears'의 값을 0으로 유지해야 한다. 즉 재고가 0이라는 사실을 기억해야 한다.  아예 키에 해당하는 항목을 삭제하는 것과 달리 여기서는 그 키의 값만 0으로 바꾸고 있다.

>>> inventory['pears'] = 0
>>> print(inventory)
{'apples': 430, 'bananas': 312, 'pears': 0, 'oranges': 525}

사전에도 len() 함수는 잘 동작한다. 그 사전이 가진 키-값 쌍의 개수를 돌려준다. 이것은 사전이 가진 키의 개수와 같다.

>>> len(inventory)
4

포함관계를 검사하는 in 연산은 그것이 사전의 키에 속하면 True, 아니면 False를 리턴한다. 여기서 주의할 점은 in 연산을 사전 변수에 대해 적용하면 키의 포함여부를 반환한다는 것이다. 

>>> 'pears' in inventory
True
>>> 'blueberries' in inventory
False

사전에 없는 키를 이용하여 값을 얻으려 하거나 []에서 주어진 키가 없으면 실행 오류가 발생한다. 그러므로 이 연산은 다른 동작을 수행하기 전에 키가 있는지 검사할 때 매우 유용한다.

>>> inventory['blueberries']
Traceback (most recent call last):
File "", line 1, in <module>
KeyError: 'blueberries'
>>>

사전에서 키를 []에 넣어서 값을 찾을 때 실행오류가 발생할 수 있는 문제를 해결하기 위해 사전에서 키에 대응하는 값을 돌려받는 빌트인 get 메소드가 따로 있다. 이 메소드는 키가 없을 때 돌려줄 디폴트 값을 매개변수로 넘길 수 있다.

>>> inventory.get('blueberries', 0)
0
>>> inventory.get('bananas', 0)
312

또한 디폴트 값을 따로 지정하지 않으면 해당 키가 없을 때 None 값을 돌려준다.

또한 사전에 대해서 리스트에서 썼던 sorted 함수를 쓸 수 있다. sorted는 매개변수로 받은 리스트는 그대로 두고 거기 포함된 값을 모두 포함하면서 정렬된 리스트를 반환값으로 돌려준다. 이러한 파이썬의 빌트인 sorted 함수는 사전에 대해서 적용되면 사전의 모든 항목을 키 순서로 소팅한 리스트를 돌려준다.

>>> sorted(inventory)
['apples', 'bananas', 'oranges', 'pears']

6.3. 사전의 지정 - 별칭(aliases)과 복사(copying)

별칭은 같은 객체를 가리키는 여러 개의 변수 이름이 있는 것을 말한다. 일반적으로 b = a 라는 지정문을 사용하면 a가 가리키는 것을 b도 가리키게 되므로 b는 a의 별칭이 된다. 그런데 사전은 수정가능하기 때문에 리스트와 마찬가지로 다른 이름으로 지정할 때 대해 주의를 기울여야 한다. 두 개 이상의 변수가 같은 사전을 가리키고 있을 때 하나를 변경하면 다른 이름에도 영향을 미친다.

예를 들어 다음과 같이 opposites라는 반대말을 가지는 사전을 생각해보자. 이것을 an_alias에 지정하면 우리는 같은 사전 객체를 두 개의 이름이 가리키게 된다.

>>> opposites = {'up': 'down', 'right': 'wrong', 'true': 'false'}
>>> an_alias = opposites

이렇게 되면 우리가 opposites에서 바꾼 결과가 an_alias에도 영향을 미치게 된다.

>>> an_alias['right'] = 'left'
>>> opposites['right']
'left'

다른 변수로 동일한 사전을 가리키면서 한 곳에서 사전을 변경한 것이 다른 곳에서 사용하는 다른 변수에 영향을 미치게 되면 프로그램을 디버깅하기가 매우 어려워진다. (an_alias를 사용하는 다른 부분, 함수나 모듈에서는 왜 키 'right'의 값이 바뀌었는지 알 수가 없다.)

그래서 사전 객체를 변경하기를 원할 때는 원본의 복사를 만들어 보관해 두기 위해 copy 메소드를 사용한다. 다음과 같이 copy() 메소드를 이용해서 복사하면 새로운 객체가 생기고 그 객체는 opposites가 가지던 키-값 쌍을 그대로 가지게 된다.

>>> a_copy = opposites.copy()

이렇게 되면 a_copy의 내부 값을 변경하더라도 opposites의 값은 그대로 남아있음을 알 수 있다.

>>> a_copy['right'] = 'privilege'
>>> opposites['right']
'left'>>> a_copy['right'] = 'privilege'>>> opposites['right']'left'

12.2. Dictionary methods

다음으로 사전 타입이 제공하는 메소드를 살펴보자. 사전은 여러가지 유용한 빌트인 메소드를 제공한다. 스트링이나 리스트에서 봤듯이 사전도 사전 이름 다음에 점이 오고 점 뒤에 메소드 이름이 나온다. 

사전의 메소드인 keys()는 사전에 대해 그 사전의 키 리스트를 결과로 돌려준다. 여기서도 돌아오는 리스트의 순서가 일정하지 않음을 알 수 있다.

>>> eng2kor.keys() 
['three', 'two', 'one']

 

이 메소드 호출에서 빈 괄호는 이 메소드가 매개변수가 없음을 나타낸다.

메소드는 객체에 대해서 불려지는데 여기서는 end2kor에 대해 keys() 메소드가 불려졌다 라고 한다. 실제로는 메소드를 호출한 객체가 이 메소드의 첫번째 매개변수로 전달된다. 이것에 대해서는 객체지향과 클래스를 다룰 때 자세하게 살펴볼 것이다.

values 메소드도 비슷해서 사전의 값의 리스트를 돌려준다. 값의 리스트의 순서는 키의 리스트의 순서와 동일하다.

>>> eng2kor.values() 
['삼', '이', '일']

items 메소드는 키와 값을 모두 돌려주는데 둘을 튜플로 묶어서 리스트로 돌려준다.

>>> eng2kor.items() 
[('three', '삼'), ('two', '이'), ('one', '일')]

12.4. 로그인 정보 저장

사전을 사용하는 예로 로그인 기능을 살펴보겠습니다. 로그인을 위해서는 아이디와 암호가 필요합니다. 아이디에 따른 암호를 저장하는 방법을 생각해 봅시다. 먼저 리스트로 이러한 정보를 저장하는 방법을 생각해 보겠습니다.

[('ami', 203), ('handy', 980), ('joe', 502)]

리스트에 아이디와 암호를 짝지어 저장하는 형태가 있겠지요? 그리고 다음과 같이 로그인 정보가 맞는지 검사할 수 있습니다. 여기서 for 문 안에서 if break를 쓰는 경우에 for else를 사용할 수 있습니다. 즉 for 문이 if 문에 한번도 안 들어가고 끝까지 수행한 후 종료하면 for else의 else 부분을 실행하게 됩니다. if break로 중간에 벗어난 경우는 else를 수행하지 않습니다. 그러므로 리스트에 아이디와 암호가 일치하는 것이 없으면 else 절로 들어가게 됩니다.

logins = [('ami', 203), ('handy', 980), ('joe', 502)]
while True:
	id = input('아이디:')
	pwd = int(input('암호:'))
	for (s, b) in logins:
		if a, b == id, login:
			print(id, '님, 어서오세요.')
			break
	else:
		print('없는 id나 잘못된 암호입니다.')

그런데 이 경우 로그인 정보를 받으면 그 아이디가 등록된 것인지와 그에 대응하는 비밀번호가 무엇인지 찾기 위해 리스트 전체를 for 루프로 차례로 검사해야 합니다. 리스트가 아주 길다면 이것은 시간이 상당히 많이 걸리는 작업이 될 것입니다.

이런 것을 키-값 쌍으로 사전에 저장하면 코드도 훨씬 간결해지고 실행 속도도 훨씬 빨라집니다.

logins = [('ami', 203), ('handy', 980), ('joe', 502)]
while True:
	id = input('아이디:')
	pwd = int(input('암호:'))
	if id not in logins:
		print('없는 아이디입니다.')
	elif logins[id] != pwd:
		print('잘못된 암호입니다.')
	else:
		print(id, '님, 어서 오세요.')

이 코드는 일단 while 문 안에 for 문을 다시 실행하지 않으므로 훨씬 효율적이겠죠? 없는 아이디인지 검사는 id not in logins로 키가 있는지로 검사할 수 있습니다. 또 id에 대응하는 암호는 logins[id]와 같이 한번에 찾아올 수가 있습니다. 

이런 로그인정보에 새로운 아이디를 추가하는 것도 쉽겠죠? 예를 들어 새로운 사용자를 추가하는 경우를 생각해 봅시다. 아이디는 기존에 없는 것이어야 하고 암호는 한번더 입력해서 일치하는지 확인하는 기능을 넣으려고 합니다.

logins = [('ami', 203), ('handy', 980), ('joe', 502)]

def add_new_user():
	id = input('새로운 아이디:')
	while id in logins:
		id = input('새로운 아이디:')
	pwd = int(input('암호:'))
	while True:
		pwd2 = int(input('암호확인:'))
		if pwd == pwd2:
			break
		pwd = int(input('암호:'))
	logins[id] = pwd
	print(id, '님, 사용자 등록이 완료되었습니다.')
    
while True:
	id = input('아이디:')
    if id == 'new':
		add_new_user()
		break
	if id == 'end':
		break
	if id not in logins:
		print('없는 아이디입니다.')
        break
	pwd = int(input('암호:'))
	if logins[id] != pwd:
		print('잘못된 암호입니다.')
	else:
		print(id, '님, 어서 오세요.')

add_new_user 함수에서는 id를 받아서 그 아이디가 logins에 있을 동안 계속 다시 입력하게 합니다. 암호에 대해서는 암호확인을 통해 일치하면 벗어나고 그렇지 않으면 계속 다시 입력하게 합니다. 여기서 새로운 사용자 아이디와 암호를 등록하게 위해 logins[id] = pwd라고 써주었습니다. 이것이 사전에 새로운 키-값 쌍을 추가하는 방법입니다. 파이썬은 사전을 마치 배열처럼 쓸 수 있게 해 주어 매우 편리합니다.

12.5. Hints

또 다른 예로 피보나치 수를 구하는 함수를 생각해 봅시다. 피보나치를 구하는 재귀 함수를 앞에서 살펴보았는데요, 다시 한번 살펴보겠습니다.

def fibonacci (n): 
	if n == 0 or n == 1: 
		return 1 
	else: 
		return fibonacci(n-1) + fibonacci(n-2)

단순한 재귀 방식으로 코딩을 하면 어마어마한 시간이 걸린다는 것을 함수 포스팅에서 살펴보았습니다. 이 코드로 fibonacci(20)은 비교적 빨리 결과가 나오지만 fibonacci(30)은 엄청나게 오래 걸리는 것을 확인할 수 있습니다. 왜 그런지 이해하기 위해 다음 그림에서 n-4일 때 재귀 함수의 호출이 어떻게 되는지 살펴보겠습니다.

이 호출 그래프는 fibonacci 함수가 호출될 때마다 네모가 하나씩 생깁니다. 화살표는 추가로 다른 함수를 재귀 호출하는 것을 보여줍니다. 제일 위에는 n = 4에서 출발하는데 거기서 n = 3과 n = 2를 호출합니다. 다음으로 n = 3에서 다시 n = 2와 n = 1을 호출하는 식입니다. fibonacci(0)과 fibonacci(1)이 얼마나 많이 호출되는지를 보면 이 방법이 시작하는 n이 커졌을 때 매우 비효율적이 될 것임을 알 수 있습니다. 

해결 방법은 이제까지 계산된 피보나치 수를 기억해 두는 방법입니다. 그러면 계산했던 것을 또 계산하지는 않겠지요? 이제까지 계산된 피보나치 수를 기억시키기 위해 사전을 이용할 수 있습니다. hint라고 하는 사전을 만들어 이제까지 계산된 것을 기억시키는 방법입니다. 

hint = {0:1, 1:1}
def fibonacci(n):
	if n in hint:
		return hint[n]
	newvalue = fibonacci(n-1) + fibonacci(n-2)
	hint[n] = newvalue
	return newvalue

여기서는 처음에 사전에 0과 1에 대한 값만 가지고 시작합니다. 이 함수가 호출될 때마다 새로운 값이 계산되면 그것은 바로 hint에 추가됩니다. 그럼 다음 값을 계산할 때 이미 계산된 피보나치 값이 있으면 그것을 이용하므로 추가적인 재귀 호출은 일어나지 않게 됩니다.

이 코드에서 중요한 것은 n에 대해 새로 계산된 피보나치 수를 반드시 사전에 등록해야 한다는 점입니다. 새로운 값을 등록하는 것은 hint[n] = newvalue로 처리했습니다. 새로운 값이 나오면 리턴하기 전에 바로 등록을 해야 다시는 그 값을 또 계산하지 않게 됩니다. 

이 코드는 n = 100일 때도 거의 바로 결과를 돌려줍니다. 왜냐하면 재귀호출을 통해 일어나는 함수 호출의 회수가 n에 선형비례하기 때문입니다.

>>> fibonacci(100)
354224848179261915075
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함