CODICT

2. Python의 자료구조 - 리스트, 튜플, 딕셔너리, 집합 본문

Programming/Python

2. Python의 자료구조 - 리스트, 튜플, 딕셔너리, 집합

Foxism 2019. 10. 5. 02:11

전 게시글에서 불리언, 정수, 부동소수점수, 문자열에 대한 기본 자료형을 다루었습니다. 이 자료형들이 원자(Atom)이라면, 이번 장에서 다룰 자료구조(Data Structure)는 분자(Molecule)입니다. 이 말은 기본 자료형들이 복잡한 형태로 결합된다는 것을 의미합니다. 이 자료구조를 매일 사용하게 될 것입니다. 대부분의 프로그래밍 과정은 데이터를 잘게 나누고, 이것들을 붙여서 특정한 형태로 만드는 것입니다.


1. 리스트와 튜플

 

대부분의 언어는 첫 번째, 두 번째, ... 그리고 마지막 항목의 정수 위치로 시퀀스의 항목을 나타냅니다. 이전 게시글에서 파이썬의 문자열은 문자의 시퀀스라는 것을 배웠습니다. 이번 장에서 리스트는 모든 것의 시퀀스라는 것을 알게 될 것입니다.

파이썬에는 두 가지 다른 시퀀스 구조가 있습니다. 튜플(Tuple)과 리스트(List)입니다. 이 구조에는 0 혹은 그 이상의 항목이 포함되어 있습니다. 문자열과는 달리, 이들 항목은 다른 타입이 될 수 있습니다. 다시 말해 각 요소는 어떤 객체도 될 수 있습니다. 이것은 프로그래머가 원하는 대로 깊고 복잡한 구조를 만들 수 있게 해줍니다.

파이썬은 왜 리스트와 튜플 모두를 포함하고 있을까요? 튜플은 불변(Immutable)합니다. 튜플에 항목을 할당하고 나서, 이를 바꿀 수 없습니다. 리스트는 가변(Mutable)입니다. 항목을 할당하고, 자유롭게 수정하거나 삭제할 수 있습니다. 리스트를 중심으로 예제를 살펴보도록 하겠습니다.

 

2. 리스트

 

리스트는 데이터를 순차적으로 파악하는 데 유용합니다. 특히 내용의 순서가 바뀔 수 있다는 점에서 유용합니다. 문자열과 달리 리스트는 변경이 가능하며, 리스트의 현재 위치에서 새로운 요소를 추가하거나 삭제 혹은 기존 요소를 덮어쓸 수 있습니다. 그리고 리스트에는 동일한 값이 여러 번 나타날 수 있습니다.

 

2.1. 리스트 생성하기: [] 또는 list()

 

리스트는 0 혹은 그 이상의 요소로 만들어집니다. 콤마( , )로 구분하고, 대괄호([])로 둘러싸여 표현됩니다. 빈 리스트를 만들고 싶다면, list() 함수를 이용하여 빈 리스트를 할당할 수 있습니다.

>>> empty_list = []
>>> family = ['Father', 'Mother', 'Me', 'Sister']
>>> notes = list()
>>> notes
#[]

물론, 리스트 컴프리헨션(List Comprehension)을 이용하여 리스트를 생성하는 방법도 있지만, 이 부분에 대해서는 추후에 작성될 게시글에서 자세히 다루도록 하겠습니다. 요소들이 순서는 상관없이 유일한 값으로만 유지되어야 한다면, 리스트보다는 나중에 언급할 집합(Set)을 사용하는 것이 좋습니다.

 

2.2. 다른 자료형을 리스트로 변환하기: list()

 

list() 함수는 다른 데이터 타입을 리스트로 변환합니다. 다음 코드는 하나의 단어를 한 문자 문자열의 리스트로 변환합니다.

>>> list('family')
#['f', 'a', 'm', 'i', 'l', 'y']

다음 코드는 튜플을 리스트로 변환합니다. (튜플은 나중에 언급하도록 하겠습니다.)

>>> family = ('father', 'mother', 'me', 'sister')
>>> list(family)
#['father', 'mother', 'me', 'sister']

지난 게시글에서 배운 문자열을 나누는 방법인 split()은 문자열을 구분자로 나누어서 리스트로 변환합니다.

>>> today = '2019/10/1'
>>> today.split('/')
#['2019', '10', '1']

문자열에 구분자가 여러 개 존재한다면 다음과 같이 리스트에 빈 문자열이 들어가게 될 것입니다.

>>> before_split = '3/14//5/5//12/25'
>>> before_split.split('/')
#['3', '14', '', '5', '5', '', '12', '25']

두 문자 문자열 //을 구분자로 사용하면 다음과 같은 결과를 얻을 수 있습니다.

>>> before_split = '3/14//5/5//12/25'
>>> before_split.split('//')
['3/14', '5/5', '12/25']

 

2.3. [offset]으로 항목 얻기

 

문자열과 마찬가지로 리스트는 오프셋으로 하나의 특정 값을 추출할 수 있습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister']
>>> family[0]
#'Father'
>>> family[1]
#'Mother'
>>> family[2]
#'Me'
>>> family[3]
#'Sister'

문자열과 마찬가지로 음수의 인덱스는 끝에서 거꾸로 값을 추출합니다.

>>> family[-1]
#'Sister'
>>> family[-2]
#'Me'
>>> family[-3]
#'Mother'
>>> family[-4]
#'Father'

 

2.4. 리스트의 리스트

 

리스트는 다음과 같이 리스트뿐만 아니라 다른 타입의 요소도 포함할 수 있습니다.

>>> toys = ['robot', 'computer']
>>> food = ['banana', 'apple']
>>> pets = ['cat', 'dog']
>>> gifts = [toys, food, 'surprise', pets]

그렇다면 리스트의 리스트인 toys는 어떻게 생겼을까요?

>>> gifts
#[['robot', 'computer'], ['banana', 'apple'], 'surprise', ['cat', 'dog']]

첫 번째 항목을 추출해 보겠습니다.

>>> gifts[0]
#['robot', 'computer']

첫 번째 항목은 리스트입니다. 다시 말해서 gifts 리스트의 첫 번재 항목인 toys 리스트입니다. 두 번째 항목을 추출해 보겠습니다.

>>> gifts[1]
#['banana', 'apple']

두 번째 항목은 foods 리스트입니다. foods 리스트의 첫 번째 항목을 gifts 리스트에서 다음과 같이 두 인덱스를 사용해서 추출할 수 있습니다.

>>> gifts[1][0]
#'banana'

[1]은 gifts의 두 번째 항목을 가리키고, [0]은 앞에서 가리킨 항목인 foods 리스트의 첫 번째 항목인 banana 문자열을 가리킵니다.

 

2.5. [offset]으로 항목 바꾸기

 

오프셋으로 항목을 얻어서 바꿀 수 있습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister']
>>> family[2] = 'You'
>>> family
#['Father', 'Mother', 'You', 'Sister']

다시 말해 리스트의 오프셋은 리스트에서 유효한 위치여야 합니다. 그렇지 않다면, list index out of range 오류를 보실 수 있습니다.

문자열은 이러한 방식으로 변경할 수 없습니다. 왜냐하면 문자열은 불변이기 때문입니다. 리스트는 변경 가능합니다. 따라서, 리스트의 항목 수와 항목 내용을 바꿀 수 있습니다.

 

2.6. 슬라이스로 항목 추출하기

 

슬라이스를 사용해서 리스트의 서브시퀀스를 추출할 수 있습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister']
>>> family[0:2]
#['Father', 'Mother']

리스트의 슬라이스 또한 리스트입니다.

문자열과 마찬가지로 슬라이스에 스텝을 사용할 수 있습니다. 다음 코드는 처음부터 오른쪽으로 2칸식 항목을 추출합니다.

>>> family[::2]
#['Father', 'Me']

다음은 끝에서 왼쪽으로 2칸씩 항목을 추출합니다.

>>> family[::-2]
#['Sister', 'Mother']

리스트를 반전시킬 수도 있습니다.

>>> family[::-1]
#['Sister', 'Me', 'Mother', 'Father']

 

2.7. 리스트의 끝에 항목 추가하기: append()

 

append()는 리스트의 끝에 새 항목을 추가합니다. 이전 코드에서 곰곰히 생각해보니 토끼를 빼먹었던 것 같습니다. 리스트는 변경 가능하기 때문에 이 오류를 정정하기 위해 토끼를 리스트에 추가할 수 있습니다.

>>> family.append('rabbit')
>>> family
#['Father', 'Mother', 'Me', 'Sister', 'rabbit']

 

2.8. 리스트 병합하기: extend() 또는 +=

 

extend()를 사용하여 다른 리스트를 병합할 수 있다. 리스트 family에 새로운 리스트 neighbors를 병합해 보겠습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> neighbors = ['Tom', 'Park', 'James']
>>> family.extend(neighbors)
>>> family
#['Father', 'Mother', 'Me', 'Sister', 'rabbit', 'Tom', 'Park', 'James']

+=를 사용하여 병합할 수도 있습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> neighbors = ['Tom', 'Park', 'James']
>>> family += neighbors
>>> family
#['Father', 'Mother', 'Me', 'Sister', 'rabbit', 'Tom', 'Park', 'James']

append()를 사용하면 항목을 병합하지 않고, neighbors가 하나의 리스트로 추가됩니다. 이 점을 유의하셔야 합니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> neighbors = ['Tom', 'Park', 'James']
>>> family.append(neighbors)
>>> family
#['Father', 'Mother', 'Me', 'Sister', 'rabbit', ['Tom', 'Park', 'James']]

이것은 리스트가 다른 타입의 요소를 포함할 수 있다는 것을 보여줍니다. family에는 5개의 문자열과 세 문자열의 리스트가 존재합니다.

 

2.9. 오프셋과 insert()로 항목 추가하기

 

append() 함수는 단지 리스트의 끝에 항목을 추가합니다. 그러나 insert() 함수는 원하는 위치에 항목을 추가할 수 있습니다. 오프셋 0은 시작 지점에 항목을 삽입한다. 리스트의 끝을 넘는 오프셋은 append()처럼 끝에 항목을 추가합니다. 그러므로 파이썬이 예외를 던질 걱정은 넣어 두셔도 좋습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister']
>>> family.insert(3, 'rabbit')
>>> family
#['Father', 'Mother', 'Me', 'rabbit', 'Sister']
>>> family.insert(100, 'dog')
>>> family
#['Father', 'Mother', 'Me', 'rabbit', 'Sister', 'dog']

 

2.10. 오프셋으로 항목 삭제하기: del

 

잘 키우던 토끼가 가출을 하였습니다. 결국 못 찾았기에, family에서 rabbit을 삭제하도록 하겠습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> del family[-1]
>>> family
#['Father', 'Mother', 'Me', 'Sister']

오프셋으로 리스트의 특정 항목을 삭제하면, 제거된 항목 이후의 항목들이 한 칸씩 앞으로 당겨집니다. 그리고 리스트의 길이가 1 감소하게 됩니다. family 리스트에서 rabbit을 제거하면 다음과 같은 결과를 얻습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> family[2]
#'Me'
>>> del family[2]
>>> family
#['Father', 'Mother', 'Sister', 'rabbit']
>>> family[2]
#'Sister'

참고로, del은 리스트 함수가 아니라 파이썬 구문입니다. 즉, family[-2].del()을 수행할 수 없습니다. del은 할당(=)의 반대입니다 .이것은 객체로부터 이름을 분리하고, (이 이름이 객체의 마지막 참조일 경우) 객체의 메모리를 비워줍니다.

 

2.11. 값으로 항목 삭제하기: remove()

 

리스트에서 삭제할 항목의 위치를 모르는 경우, remove()와 값으로 그 항목을 삭제할 수 있습니다. rabbit을 삭제해보겠습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> family.remove('rabbit')
>>> family
#['Father', 'Mother', 'Me', 'Sister']

 

2.12. 오프셋으로 항목을 얻은 후 삭제하기: pop()

 

pop()은 리스트에서 항목을 가져오는 것과 동시에 그 항목을 삭제합니다. 오프셋과 함께 pop()을 호출했다면 그 오프셋의 항목이 반환됩니다. 인자가 없다면 -1을 사용합니다. pop(0)은 리스트의 헤드(head; 머리, 시작)를 반환합니다. 그리고 pop() 혹은 pop(-1)은 리스트의 테일(tail; 꼬리, 끝)을 반환합니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> family.pop()
#'rabbit'
>>> family
#['Father', 'Mother', 'Me', 'Sister']
>>> family.pop(1)
#'Mother'
>>> family
#['Father', 'Me', 'Sister']

참고로, 만약 append()로 새로운 항목을 끝에 추가한 뒤 pop()으로 다시 마지막 항목을 제거했다면, 여러분은 LIFO(Last In First Out; 후입선출) 자료구조인 스택(Stack)을 구현한 것입니다. 그리고 pop(0)을 사용했다면 FIFO(First In First Out; 선입선출) 자료구조인 큐(Queue)를 구현한 것입니다. 이들은 수집한 데이터에서 가장 오래된 것을 먼저 사용하거나(FIFO), 최근 것을 먼저 사용할 때(LIFO) 유용합니다.

 

2.13. 값으로 항목 오프셋 찾기: index()

 

항목 값의 리스트 오프셋을 알고 싶다면 index()를 사용하면 됩니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> family.index('Me')
#2

 

2.14. 존재여부 확인하기: in

 

리스트에서 어떤 값의 존재를 확인하려면 in을 사용합니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> 'Me' in family
#True

리스트에는 같은 값이 여러 개 존재할 수 있습니다. 리스트에 값이 적어도 하나 존재하면 in에 대한 연산은 True를 반환합니다.

>>> words = ['park', 'bark', 'bak', 'park']
>>> 'park' in words
#True

항목에 순서에 상관없이 리스트의 존재 여부만 확인하고자 하면 집합(set)은 유일한 값을 저장하고 조회하는 데 있어 리스트보다 더 좋은 방법입니다. 집합에 대해서는 잠시 후에 설명하도록 하겠습니다.

 

2.15. 값 세기: count()

 

리스트에 특정 값이 얼마나 있는지 세기 위해서는 count()를 사용합니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> family.count('Me')
#1

 

2.16. 문자열로 변환하기: join()

 

join()에 대해서는 이전 게시물이 더 자세하게 설명되어 있습니다. 여기서는 또 다른 코드를 살펴보도록 하겠습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> ', '.join(family)
#'Father, Mother, Me, Sister, rabbit'

이 코드는 앞에서 본 것과 다를 게 없는 것 같습니다. join()은 문자열 메서드지, 리스트 메서드가 아닙니다. family.join(', ')이 조금 더 직관적으로 보일지라도, 이렇게 사용할 수 없습니다. join()의 인자는 문자열 혹은 반복 가능한(Iterable) 문자열의 시퀀스(리스트 포함)입니다. 그리고 결과로 반환되는 값은 문자열입니다. join()이 리스트 메서드였다면, 튜플과 문자열 같은 다른 반복 가능한 객체와 함께 사용하지 못할 것입니다. 어떤 반복 가능한 타입을 처리하기 위해 각 타입을 실제로 병합할 수 있도록 특별한 코드가 필요했습니다. 다음 코드와 같이 join()을 split()의 반대라고 생각하면 기억하기 쉬울 것 같습니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> separator = ' * '
>>> joined = separator.join(family)
>>> joined
#'Father * Mother * Me * Sister * rabbit'
>>> separated = joined.split(separator)
>>> separated
#['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> separated == family
#True

 

 

2.17. 정렬하기: sort()

 

오프셋을 이용하여 리스트를 정렬할 때도 있지만, 값을 이용하여 리스트를 정렬할 때도 있습니다. 파이썬은 두 가지 함수를 제공합니다.

  • sort()는 리스트 자체를 내부적으로 정렬합니다.

  • sorted()는 리스트의 정렬된 복사본을 반환합니다.

리스트의 항목이 숫자인 경우, 기본적으로 오름차순으로 정렬합니다. 문자열인 경우, 알파벳순으로 정렬합니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> sorted_family = sorted(family)
>>> sorted_family
#['Father', 'Me', 'Mother', 'Sister', 'rabbit']

sorted_family는 복사본입니다. 원본 리스트는 변하지 않았습니다.

>>> family
#['Father', 'Mother', 'Me', 'Sister', 'rabbit']

하지만 sort()는 family를 정렬된 family로 바꿉니다.

>>> family.sort()
>>> family
#['Father', 'Me', 'Mother', 'Sister', 'rabbit']

family와 같이 리스트의 요소들이 모두 같은 타입일 경우 sort()는 제대로 동작합니다. 때때로 정수와 부동소수점수 같이 혼합된 타입도 정렬할 수 있습니다. 파이썬이 자동으로 타입을 형변환해서 항목들을 정렬하기 때문입니다.

>>> nums = [1, 2, 6, 5.3, 7, 3]
>>> nums.sort()
>>> nums
#[1, 2, 3, 5.3, 6, 7]

기본 정렬 방식은 오름차순입니다. 내림차순으로 정렬하고 싶다면 인자에 reverse=True를 추가하면 됩니다.

>>> nums = [1, 2, 6, 5.3, 7, 3]
>>> nums.sort(reverse=True)
>>> nums
#[7, 6, 5.3, 3, 2, 1]

 

2.18. 항목 개수 얻기: len()

 

len()은 리스트의 항목 수를 반환합니다.

>>> family = ['Father', 'Mother', 'Me', 'Sister', 'rabbit']
>>> len(family)
#5

 

2.19. 할당: =, 복사: copy()

 

다음 코드와 같이 한 리스트를 변수 두 곳에 할당했을 경우, 한 리스트를 변경하면 다른 리스트도 따라서 변경됩니다.

>>> a = [1, 2, 3]
>>> a
#[1, 2, 3]
>>> b = a
>>> b
#[1, 2, 3]
>>> a[0] = 'surprise'
>>> a
#['surprise', 2, 3]

b에는 무엇이 있을까요? 여전히 [1, 2, 3]일까요, 아니면 ['surprise', 2, 3]일까요?

>>> b
#['surprise', 2, 3]

전 게시글에서 이름을 상자에 붙인 스티커에 비유한 것을 기억하시나요? b는 단지 같은 리스트 객체의 a를 참조합니다. 그러므로 a혹은 b 리스트의 내용을 변경하면 두 변수 모두에 반영됩니다.

>>> b
#['surprise', 2, 3]
>>> b[0] = 'I hate surprises'
>>> b
#['I hate surprises', 2, 3]
>>> a
#['I hate surprises', 2, 3]

다음과 같은 방법을 이용하여 한 리스트를 새로운 리스트로 복사할 수 있습니다.

  • 함수 copy()

  • 변환 함수 list()

  • 슬라이스 [ : ]

원본 리스트 a가 있습니다. copy()로 리스트 b를 만듭니다. list()변환 함수로 리스트 c를 만듭니다. 그리고 a를 슬라이스해서 리스트 d를 만듭니다.

>>> a = [1, 2, 3]
>>> b = a.copy()
>>> c = list(a)
>>> d = a[:]

b, c, d는 a의 복사본입니다. 이들은 자신만의 값을 가진 새로운 객체 입니다. 그리고 원본 리스트 객체 [1, 2, 3]을 참조하는 아무런 참조가 없습니다. 복사본 b, c, d를 바꾸더라도 원본 a에는 아무런 영향을 주지 않습니다.

>>> a[0] = 'Integer lists are boring'
>>> a
#['Integer lists are boring', 2, 3]
>>> b
#[1, 2, 3]
>>> c
#[1, 2, 3]
>>> d
#[1, 2, 3]

 

3. 튜플

 

리스트와 마찬가지로 튜플(Tuple)은 임의적인 항목의 시퀀스입니다. 리스트와는 다르게 튜플은 불변합니다. 이 말은 튜플을 정의한 후에는 추가, 삭제, 수정을 할 수 없다는 것을 의미합니다. 그러므로 튜플은 상수의 리스트라고 할 수 있습니다.

 

3.1. 튜플 생성하기: ()

 

이번 코드를 보면 알겠지만, 튜플의 구문은 리스트와 조금 다릅니다. 먼저 ()로 튜플을 만들어 보겠습니다.

>>> empty_tuple = ()
>>> empty_tuple
#()

하나 이상의 요소가 있는 튜플을 만들기 위해서는 각 요소 뒤에 콤마(,)를 붙입니다. 튜플에 하나의 요소를 저장해보도록 하겠습니다.

>>> one = 'present',
>>> one
#('present',)

두 개 이상의 요소가 있을 경우, 마지막 요소에는 콤마를 붙이지 않습니다.

>>> multi = 'past', 'present', 'future'
>>> multi
#('past', 'present', 'future')

파이썬은 튜플을 출력할 때 소괄호를 포함합니다. 튜플을 정의할 때는 소괄호가 필요 없습니다. 뒤에 콤마가 붙는다는 것은 튜플을 정의한다는 뜻입니다. 그러나 값들을 괄호로 묶어서 튜플을 정의한다면, 이것이 튜플인지 구분하기가 더 쉽습니다. 튜플은 한 번에 여러 변수를 할당할 수 있으며, 이를 튜플 언패킹(Tuple unpacking)이라고 합니다.

>>> a, b, c = multi
>>> a
#'past'
>>> b
#'present'
>>> c
#'future'

한 문장에서 값을 교환하기 위해 임시 변수를 사용하지 않고 튜플을 사용할 수 있습니다.

>>> toy = 'robot'
>>> food = 'apple'
>>> toy, food = food, toy
>>> toy
#'apple'
>>> food
#'robot'

tuple()은 다른 객체를 튜플로 만들어줍니다.

>>> food_list = ['apple', 'grape', 'banana', 'chocolate']
>>> tuple(food_list)
#('apple', 'grape', 'banana', 'chocolate')

 

3.2. 튜플과 리스트

 

리스트를 대신해서 튜플을 사용할 수 있습니다. 하지만 튜플은 리스트의 append(), insert()등과 같은 함수가 없고, 함수의 수가 매우 적습니다. 튜플을 생성한 후에는 수정할 수 없기 때문입니다. 그러면 튜플은 언제 사용할까요?

  • 튜플은 리스트에 비해 더 적은 공간을 사용합니다.

  • 실수로 튜플의 내용이 바뀔 일이 없습니다.

  • 튜플을 딕셔너리의 키로 사용할 수 있습니다.

  • 네임드 튜플(Named tuple; 추후에 자세히 다루도록 하겠습니다.)은 객체의 단순한 대안이 될 수 있습니다.

  • 함수의 인자들은 튜플로 전달됩니다(함수는 추후에 다룰 예정입니다).

튜플에 대한 내용은 여기서 마치도록 하겠습니다. 일반적으로는 리스트와 딕셔너리를 더 많이 사용합니다.

 

4. 딕셔너리

 

딕셔너리(Dictionary)는 리스트와 비슷합니다. 다른 점은 항목의 순서를 따지지 않으며, 0 또는 1과 같은 오프셋으로 항목을 선택할 수 없습니다. 대신 값(Value)에 대응되는 고유한 키(Key)를 지정합니다. 이 키는 대부분 문자열이지만, 상황에 따라서는 불변하는 파이썬의 다른 타입(부울, 정수, 부동소수점수, 튜플, 문자열 등)이 될 수도 있습니다. 딕셔너리를 변경 가능하므로 키와 그에 대응되는 값의 요소를 추가, 삭제, 수정할 수 있습니다. 지금까지 배열이나 리스트만 지원하는 언어를 사용했다면, 아마 딕셔너리가 꽤 매력적으로 다가오지 않을까 싶습니다.

 

4.1. 딕셔너리 생성하기: {}

 

딕셔너리를 생성하기 위해서는 중괄호({}) 안에 콤마로 구분된 키:값 쌍을 지정합니다. 물론 속이 빈 딕셔너리도 생성할 수 있습니다.

>>> empty_dict = {}
>>> empty_dict
#{}

다음과 같이, 딕셔너리를 생성할 수 있습니다. 생성된 딕셔너리의 이름을 대화식 인터프리터에 입력할 경우 모든 키와 값을 출력합니다.

>>> favorite_food = {'dad':'apple', 'mom':'banana', 'me':'orange', 'sister':'pizza'}
>>> favorite_food
#{'dad': 'apple', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza'}

 

4.2. 딕셔너리로 변환하기: dict()

 

dict()함수를 사용해서 두 값으로 이루어진 시퀀스를 딕셔너리로 변환할 수 있습니다. 이 경우 각 시퀀스의 첫 번째 항목은 키(Key)로, 두 번째 항목은 값(Value)으로 사용됩니다.

>>> abc = [['a', 'b'], ['c', 'd'], ['e', 'f']]
>>> dict(abc)
#{'a': 'b', 'c': 'd', 'e': 'f'}

참고로, 딕셔너리의 키 순서는 임의적입니다. 그리고 항목을 어떻게 추가하느냐에 따라 순서가 달라질 수 있습니다. 다음은 두 항목 튜플로 된 리스트입니다.

>>> lt = [('a','b'),('c','d'),('e','f')]
>>> dict(lt)
#{'a': 'b', 'c': 'd', 'e': 'f'}

다음은 두 항목 리스트로 된 튜플입니다.

>>> tl = (['a','b'],['c','d'],['e','f'])
>>> dict(tl)
#{'a': 'b', 'c': 'd', 'e': 'f'}

다음은 두 문자 문자열로 된 리스트입니다.

>>> ls = ['ab','cd','ef']
>>> dict(ls)
#{'a': 'b', 'c': 'd', 'e': 'f'}

다음은 두 문자 문자열로 된 튜플입니다.

>>> ts = ('ab','cd','ef')
>>> dict(ts)
#{'a': 'b', 'c': 'd', 'e': 'f'}

추후 다른 게시글에서 소개할 zip()함수로 두 항목 시퀀스를 쉽게 생성하는 방법에 대해 설명하겠습니다.

 

4.3. 항목 추기 / 변경하기: [key]

 

딕셔너리에 항목을 추가하는 것은 간단합니다. 키에 의해 참조되는 항목에 값을 할당하면 됩니다. 키가 딕셔너리에 이미 존재하는 경우, 그 값은 새로운 값으로 대체됩니다. 키가 존재하지 않는 경우, 새 값이 키와 딕셔너리에 추가됩니다. 리스트와 달리 딕셔너리를 할당할 때는 인덱스의 범위 지정이 벗어났다는 예외에 대해 걱정할 필요가 없습니다. 다음 코드를 살펴보겠습니다.

>>> favorite_food = {'dad':'apple', 'mom':'banana', 'me':'orange', 'sister':'pizza'}
>>> favorite_food
#{'dad': 'apple', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza'}

여기서 친구를 추가하도록 하겠습니다.

>>> favorite_food['friend'] = 'magic'
>>> favorite_food
#{'dad': 'apple', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza', 'friend': 'magic'}

앗, 음식을 넣어야 하는데 다른 것을 넣었군요! 수정하도록 하겠습니다.

>>> favorite_food['friend'] = 'chocolate'
>>> favorite_food
#{'dad': 'apple', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza', 'friend': 'chocolate'}

'friend'라는 같은 키를 사용하여 'magic'을 'chochlate'로 바꿨습니다. 딕셔너리의 키들은 반드시 유일해야 합니다. 그렇지 않다면, 위에서 설명한 것처럼 나중에 들어온 값으로 기존의 값이 대체가 됩니다.

 

4.4. 딕셔너리 결합하기: update()

 

update() 함수는 한 딕셔너리의 키와 값들을 복사해서 다른 딕셔너리에 붙여줍니다. favorite_food 딕셔너리의 항목들을 정의해보도록 하겠습니다.

>>> favorite_food = {'dad':'apple', 'mom':'banana', 'me':'orange', 'sister':'pizza'}
>>> favorite_food
#{'dad': 'apple', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza'}

반 친구들의 것도 추가를 해보겠습니다.

>>> classmate = {'friend1':'cake', 'friend2':'snack'}
>>> favorite_food.update(classmate)
>>> favorite_food
#{'dad': 'apple', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza', 'friend1': 'cake', 'friend2': 'snack'}

두 번째 딕셔너리를 첫 번째 딕셔너리에 병합하려고 할 때, 두 딕셔너리에 중복된 키값이 있다면 두 번째 딕셔너리에 있는 값이 저장됩니다.

>>> first = {'a':'1', 'b':'2'}
>>> second = {'b':'3'}
>>> first.update(second)
>>> first
#{'a': '1', 'b': '3'}

 

4.5. 키와 del로 항목 삭제하기

 

만약 딕셔너리에서 지우고 싶은 항목이 있을경우, 키와 del을 사용하여 항목을 삭제할 수 있습니다. favorite_food에 추가된 friend1와 friend2의 키와 값을 삭제해 보도록 하겠습니다.

>>> del favorite_food['friend2']
>>> favorite_food
#{'dad': 'apple', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza', 'friend1': 'cake'}
>>> del favorite_food['friend1']
>>> favorite_food
#{'dad': 'apple', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza'}

 

4.6. 모든 항목 삭제하기: clear()

 

딕셔너리에 있는 키와 값을 모두 삭제하기 위해서는 clear()를 사용하거나 빈 딕셔너리({})를 이름에 할당하면 됩니다.

>>> favorite_food.clear()
>>> favorite_food
#{}
>>> favorite_food = {}
>>> favorite_food
#{}

 

4.7. in으로 키 멤버십 테스트하기

 

딕셔너리에 키가 존재하는지 알고 싶다면 in을 사용합니다.

>>> favorite_food = {'dad':'apple', 'mom':'banana', 'me':'orange', 'sister':'pizza'}
>>> 'dad' in favorite_food
#True

 

4.8. 항목 얻기: [key]

 

아마 이것이 딕셔너리를 활용하는 가장 일반적인 용도라고 생각됩니다. 딕셔너리에 키를 지정하여 이에 대응하는 값을 얻을 수 있습니다.

>>> favorite_food['dad']
#'apple'

딕셔너리에 키가 존재하지 않으면 오류를 반환합니다.

>>> favorite_food['dog']
#Traceback (most recent call last):
#  File "<pyshell#77>", line 1, in <module>
#    favorite_food['dog']
#KeyError: 'dog'

이 오류를 피할 수 있는 방법은, 이전 절에서 본 in으로 키에 대한 멤버십(Membership) 테스트를 한 후, 그에 따른 반환 값으로 진행하는 것입니다. 두 번째 방법으로는 딕셔너리의 get() 함수를 사용하는 것입니다. 이 함수는 딕셔너리, 키, 옵션값을 사용합니다. 만약 키가 존재하면 그 값을 얻고, 키가 존재하지 않는다면 옵션값을 지정해서 출력할 수 있습니다.

>>> favorite_food.get('dog', '이 항목에 없는 키 입니다.')
#'이 항목에 없는 키 입니다.'

옵션값을 지정하지 않으면 None을 얻습니다. 대화식 인터프리터에는 아무것도 표시되지 않습니다.

 

4.9. 모든 키 얻기: keys()

 

딕셔너리의 모든 키를 가져오기 위해서는 keys()를 사용합니다. 

>>> favorite_food.keys()
#dict_keys(['dad', 'mom', 'me', 'sister'])

 

4.10. 모든 값 얻기: values()

 

딕셔너리의 모든 값을 가져오기 위해서는 values()를 사용합니다.

>>> favorite_food.values()
#dict_values(['apple', 'banana', 'orange', 'pizza'])

 

4.11. 모든 쌍의 키값 얻기: items()

 

딕셔너리의 모든 쌍을 가져오기 위해서는 items()를 사용합니다.

>>> favorite_food.items()
#dict_items([('dad', 'apple'), ('mom', 'banana'), ('me', 'orange'), ('sister', 'pizza')])

각 키와 값은 튜플로 반환이 됩니다.

 

4.12. 할당: =, 복사: copy()

 

리스트와 마찬가지로 딕셔너리를 할당한 후 변경할 때 딕셔너리를 참조하는 모든 이름에 변경된 딕셔너리를 반영합니다.

>>> favorite_food = {'dad':'apple', 'mom':'banana', 'me':'orange', 'sister':'pizza'}
>>> save_food = favorite_food
>>> favorite_food['dad'] = 'peach'
>>> save_food
#{'dad': 'peach', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza'}

그러므로 딕셔너리의 키와 값을 또 다른 딕셔너리로 복사하기 위해서는 위와 같이 할당하지 않고 copy()를 사용합니다.

>>> favorite_food = {'dad':'apple', 'mom':'banana', 'me':'orange', 'sister':'pizza'}
>>> copy_food = favorite_food.copy()
>>> favorite_food['dad'] = 'peach'
>>> favorite_food
#{'dad': 'peach', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza'}
>>> copy_food
#{'dad': 'apple', 'mom': 'banana', 'me': 'orange', 'sister': 'pizza'}

 

5. 집합

 

집합(Set)은 값은 버리고 키만 남은 딕셔너리와 같습니다. 딕셔너리와 마찬가지로 각 키는 유일해야 합니다. 어떤 것이 존재하는지 여부만 판단하기 위해서는 집합을 사용합니다. 그리고 키에 어떤 정보를 첨부해서 그 결과를 얻고 싶으면 딕셔너리를 사용합니다. 집합에 대해서는 학창시절에 배웠을 것입니다. 같은 키를 갖고 있는 두 개의 집합을 결합한다고 가정하면, 집합은 각 항목이 유일해야 하기 때문에 두 집합의 합집합은 하나의 키만 포함합니다. 널(Null)혹은 빈 집합은 항목이 하나도 없는 집합입니다.

 

5.1. 집합 생성하기: set()

 

집합을 생성할 때는 다음과 같이 set() 함수 혹은 중괄호 ({}) 안에 콤마로 구분된 하나 이상의 값을 넣으면 됩니다.

>>> empty_set = set()
>>> empty_set
#set()
>>> even_numbers = {0, 2, 4, 6, 8}
>>> even_numbers
#{0, 2, 4, 6, 8}
>>> odd_numbers = {1, 3, 5, 7, 9}
>>> odd_numbers
#{1, 3, 5, 7, 9}

딕셔너리의 키와 마찬가지로 집합은 순서가 없습니다. 참고로, []가 빈 리스트를 생성하기 때문에 {}도 빈 집합을 생성할 것이라고 추측할 수도 있습니다. 그러나 {}은 빈 딕셔너리를 생성합니다. 이것이 인터프리터가 빈 집합을 {} 대신 set()으로 출력하는 이유이기도 합니다. 왜 이렇게 되었을까요? 그냥 단순하게 딕셔너리가 먼저 등장해서 중괄호를 선점하고 있었기 때문입니다.

 

5.2. 데이터 타입 변환하기: set()

 

리스트, 문자열, 튜플, 딕셔너리로부터 중복된 값을 버린 집합을 생성할 수 있습니다. 먼저 'cocacola'의 알파벳 중 하나 이상 나온 문자를 살펴보도록 하겠습니다.

>>> set('cocacola')
#{'l', 'c', 'o', 'a'}

'cocacola'에 'c'와 'a'와 'o'가 여러개가 있어도, 집합에는 이 문자들이 하나씩만 포함되어 있습니다. 이제 리스트를 집합으로 만들어 보도록 하겠습니다.

>>> set(['abc', 'cd', 'ab', 'abc', 'cd'])
#{'abc', 'cd', 'ab'}

이번에는 튜플을 집합으로 만들어 보도록 하겠습니다.

>>> set(('abc', 'cd', 'ab', 'abc', 'cd'))
#{'abc', 'cd', 'ab'}

딕셔너리에 set()을 사용하면 키만 사용합니다.

>>> set({'dad':'apple', 'mom':'banana', 'me':'orange', 'sister':'pizza'})
#{'dad', 'sister', 'me', 'mom'}

 

5.3. in으로 값 멤버십 테스트하기

 

이것은 일반적으로 사용되는 집합의 용도입니다. drinks라는 딕셔너리를 만들어 보도록 하겠습니다. 각 키는 혼합 음료의 이름이고, 값은 음료에 필요한 재료들의 집합입니다.

>>> drinks = {
	'martini':{'vodka', 'vermouth'},
	'black russian':{'vodka', 'kahlua'},
	'white russian':{'cream', 'kahlua', 'vodka'},
	'manhattan':{'rye', 'vermouth', 'bitters'},
	'screwdriver':{'orange juice', 'vodka'}
	}

중괄호가 두 번 둘러싸여 있지만, 집합은 값들의 시퀀스일 뿐이고, 딕셔너리는 하나 이상의 키:값의 쌍으로 이루어져 있습니다. 보드카(Vodka)가 포함된 음료는 무엇일까요?

>>> for name, contents in drinks.items():
	if 'vodka' in contents:
		print(name)

		
#martini
#black russian
#white russian
#screwdriver

for, if, and, or에 대해서는 다음 게시글에서 설명하도록 하겠습니다. 보드카(Vodka)를 원하지만 베르무트(Vermouth)는 싫어한다면, 다음과 같이 구할 수도 있습니다.

>>> for name, contents in drinks.items():
	if'vodka' in contents and not 'vermouth' in contents:
		print(name)

		
#black russian
#white russian
#screwdriver

 

5.4. 콤비네이션과 연산자

 

집합 값의 콤비네이션(Combination; 조합)을 어떻게 확인할까요? 오렌지 주스 혹은 베르무트(Vermouth)가 들어 있는 음료를 마시고 싶다고 가정해 봅시다. 집합 교집합 연산자(Set intersection operator)인 앰퍼샌드(&)를 사용해서 원하는 음료를 찾아보도록 하겠습니다.

>>> for name, contents in drinks.items():
	if contents & {'vermouth', 'orange joice'}:
		print(name)

		
#martini
#manhattan

& 연산자의 결과는 우리가 비교하고자 했던 두 재료의 모든 항목이 포함된 집합입니다. contents에 두 재료가 없다면, &는 False로 간주되는 빈 집합을 반환합니다. 이제 이전 절에서 본 코드를 좀 더 간결하게 작성해 보겠습니다. 보드카(Vodka)는 들어 있지만, 크림(Cream)이나 베르무트(Vermouth)는 들어 있지 않는 음료를 찾아봅시다.

>>> for name, contents in drinks.items():
	if 'vodka' in contents and not contents & {'vermouth', 'cream'}:
		print(name)

		
#black russian
#screwdriver

두 음료의 재로 집합을 변수에 집합해 보도록 하겠습니다.

>>> bruss = drinks['black russian']
>>> wruss = drinks['white russian']

다음 코드에서 집합 연산자의 기호와 함수 둘 다 사용해 보겠습니다. 1, 2가 있는 a 집합과 2, 3이 있는 b 집합을 대상으로 하겠습니다.

>>> a = {1, 2}
>>> b = {2, 3}

& 연산자와 intersection() 함수를 사용해서 교집합(Intersection; 양 집합에 모두 들어 있는 원소들의 집합)을 구해보도록 하겠습니다.

>>> a & b
#{2}
>>> a.intersection(b)
#{2}

다음 코드에서는 조금 전에 저장했던 음료 변수를 사용했습니다.

>>> bruss & wruss
#{'vodka', 'kahlua'}

이번에는 | 연산자와 union() 함수를 사용해서 합집합(Union; 각 집합의 모든 원소들을 모은 집합)을 구해보도록 하겠습니다.

>>> a | b
#{1, 2, 3}
>>> a.union(b)
#{1, 2, 3}

음료 변수에도 적용해 보도록 하겠습니다.

>>> bruss | wruss
#{'cream', 'vodka', 'kahlua'}

- 연산자와 difference()를 사용해서 차집합(Difference; 첫 번째 집합에는 있지만, 두 번째 집합에는 없는 원소들을 모은 집합)을 구해보겠습니다.

>>> a - b
#{1}
>>> a.difference(b)
#{1}
>>> bruss - wruss
#set()
>>> wruss - bruss
#{'cream'}

지금까지는 일반적인 집합 연산의 합집합, 교집합, 차집합에 대해 살펴봤습니다. 이어지는 코드에서는 그 외의 집합 연산에 대해 알아보겠습니다.

^ 연산자는 symmetric_difference() 함수를 사용하여 대칭 차집합(Exclusive; 한 쪽 집합에는 들어 있지만 양쪽 모두에 들어있지는 않은 원소들의 집합)를 구해보도록 하겠습니다.

>>> a ^ b
#{1, 3}
>>> a.symmetric_difference(b)
#{1, 3}
>>> bruss ^ wruss
#{'cream'}

<= 연산자는 issubset() 함수를 사용해서 첫 번째 집합이 두 번째 집합의 부분집합(Subset)인지 판별할 수 있습니다.

>>> a<= b
#False
>>> a.issubset(b)
#False

블랙 러시안(Black russian)에 크림(Cream)을 추가하면 화이트 러시안(White russian)이 됩니다. 그래서 wruss는 bruss의 상위집합(Superset)입니다.

>>> bruss <= wruss
#True

모든 집합은 자신의 부분집합일까요? 맞습니다.

>>> a <= a
#True
>>> a.issubset(a)
#True

첫 번째 집합이 두 번째 집합의 진부분집합(Proper subset)이 되려면 두 번째 집합에는 첫 번째 집합의 모든 원소를 포함한 그 이상의 원소가 있어야 합니다. 이번에는 < 연산자를 사용해 보도록 하겠습니다.

>>> a < b
#False
>>> a < a
#False
>>> bruss < wruss
#True

상위집합은 부분집합의 반대입니다. >=나 issuperset() 함수를 사용해서 첫 번째 집합이 두 번째 집합의 상위집합인지 살펴보겠습니다.

>>> a >= b
#False
>>> a.issuperset(b)
#False
>>> wruss >= bruss
#True

모든 집합은 자신의 상위집합입니다.

>>> a >= a
#True
>>> a.issuperset(a)
#True

마지막으로 > 연산자를 사용해서 첫 번째 집합이 두 번째 집합의 진부분집합인지 확인해보겠습니다. 첫 번째 집합이 두 번째 집합의 진부분집합이 되려면, 첫 번째 집합에는 두 번째 집합의 모든 원소를 포함한 그 이상의 원소가 있어야 합니다.

>>> a > b
#False
>>> wruss > bruss
#True

모든 집합은 자신의 진부분집합이 될 수 없습니다.

>>> a > a
#False