데이터 구조는 프로그래밍의 핵심입니다. 데이터를 효율적으로 구성, 저장, 관리하여 접근과 수정이 용이하도록 해줍니다.
주간 세탁물을 예로 들어보겠습니다. 이상적으로는 양말, 티셔츠, 바지, 속옷 등을 별도의 서랍에 정리해두면 아침에 외출 준비할 때 쉽게 찾을 수 있습니다. 이것이 바로 데이터 구조를 활용하는 것입니다. 반대로 모든 옷을 하나의 서랍에 뒤섞어 넣거나, 여러 서랍에 무작위로 넣어둔다면 출근이나 약속 준비를 위해 옷을 찾는 데 얼마나 많은 시간이 걸릴까요? 이것이 데이터 구조 없이 데이터를 관리하는 상황입니다.
이 글에서는 파이썬이 제공하는 내장 데이터 구조를 심도 있게 살펴보겠습니다.
파이썬의 내장 데이터 구조
파이썬의 주요 내장 데이터 구조로는 리스트(List), 딕셔너리(Dictionary), 튜플(Tuple), 셋(Set)이 있습니다. 각각 고유한 특성과 용도를 가지고 있어 상황에 맞게 선택하여 사용합니다.
리스트 (List)
리스트는 변경 가능(mutable)한 순서 있는 컬렉션으로, 서로 다른 데이터 타입의 요소를 한 리스트에 담을 수 있습니다.
my_list = ['문자열', 300, (2, 4), '앞의 데이터 타입은 튜플']
리스트는 요소를 추가, 삭제, 조작하기 위한 11개의 주요 메서드를 제공합니다.
요소 추가하기
- append(): 리스트 끝에 단일 항목을 추가합니다.
my_list = ['문자열, 다음은 튜플', (2, 1), 3]
my_list.append(500)
print(my_list)
# 출력: ['문자열, 다음은 튜플', (2, 1), 3, 500]
- extend(): 반복 가능한 객체의 모든 항목을 리스트에 추가합니다.
append()와 달리 인자로 받은 시퀀스의 요소를 개별적으로 풀어서 추가합니다.
my_list = ['문자열, 다음은 튜플', (2, 1), 3]
my_list.append((8, 9))
print(my_list)
# 출력: ['문자열, 다음은 튜플', (2, 1), 3, (8, 9)]
# append()는 (8, 9)를 튜플 그대로 추가
my_list = ['문자열, 다음은 튜플', (2, 1), 3]
my_list.extend((8, 9))
print(my_list)
# 출력: ['문자열, 다음은 튜플', (2, 1), 3, 8, 9]
# extend()는 튜플을 풀어서 8, 9를 개별 요소로 추가
- insert(): 지정한 인덱스 위치에 요소를 삽입합니다. 첫 번째 인자는 인덱스, 두 번째 인자는 삽입할 요소입니다.
my_list = ['문자열, 다음은 튜플', (2, 1), 3]
my_list.insert(0, 700)
print(my_list)
# 출력: [700, '문자열, 다음은 튜플', (2, 1), 3]
# 0번 인덱스에 700 삽입
요소 삭제하기
- remove(): 리스트에서 처음으로 일치하는 값의 요소를 삭제합니다.
my_list = ['문자열, 다음은 튜플', (2, 1), 3, 8, 3]
my_list.remove(3)
print(my_list)
# 출력: ['문자열, 다음은 튜플', (2, 1), 8, 3]
# 첫 번째 3만 삭제됨
- pop(): 지정한 인덱스의 요소를 삭제하고 반환합니다. 인덱스를 생략하면 마지막 요소를 삭제합니다.
my_list = ['문자열, 다음은 튜플', (2, 1), 3]
my_list.pop(0)
print(my_list)
# 출력: [(2, 1), 3]
my_list = ['문자열, 다음은 튜플', (2, 1), 3]
my_list.pop()
print(my_list)
# 출력: ['문자열, 다음은 튜플', (2, 1)]
- clear(): 리스트의 모든 요소를 삭제합니다.
my_list = ['문자열, 다음은 튜플', (2, 1), 3]
my_list.clear()
print(my_list)
# 출력: []
기타 유용한 리스트 메서드
- index(): 지정한 값의 첫 번째 인덱스를 반환합니다.
my_list = [8, 20, 1, 9, 2, 3, 937, 0]
print(my_list.index(9))
# 출력: 3
- count(): 리스트 내 특정 값의 출현 횟수를 셉니다.
my_list = [8, 20, 1, 8, 2, 8, 937, 8]
print(my_list.count(8))
# 출력: 4
- sort(): 리스트를 정렬합니다. 인자 없이 오름차순,
reverse=True로 내림차순 정렬이 가능합니다. 원본 리스트를 변경합니다.
my_list = [8, 20, 1, 9, 2, 3, 937, 0]
my_list.sort()
print(my_list)
# 출력: [0, 1, 2, 3, 8, 9, 20, 937]
my_list.sort(reverse=True)
print(my_list)
# 출력: [937, 20, 9, 8, 3, 2, 1, 0]
- reverse(): 리스트의 순서를 뒤집습니다. 정렬이 아닌 단순히 순서만 반전시킵니다.
my_list = [8, 20, 1, 9, 2, 3, 937, 0]
my_list.reverse()
print(my_list)
# 출력: [0, 937, 3, 2, 9, 1, 20, 8]
- copy(): 리스트의 얕은 복사본을 반환합니다.
my_list = [8, 20, 1, 9, 2, 3, 937, 0]
new_list = my_list.copy()
print(new_list)
# 출력: [8, 20, 1, 9, 2, 3, 937, 0]
튜플 (Tuple)
튜플은 괄호 ()로 감싸진 불변(immutable) 시퀀스입니다. 리스트와 달리 한 번 생성하면 내용을 변경할 수 없으며, 이로 인해 리스트보다 메모리 효율이 좋고 처리 속도가 빠릅니다. 불변성이 보장되므로 딕셔너리의 키(key)로 사용할 수 있으며, 함수에서 여러 값을 반환할 때도 유용합니다.
튜플에 데이터를 추가하려면 연결(concatenation) 연산을 통해 새로운 튜플을 생성해야 합니다.
my_tuple = (1, 2, 3)
print(my_tuple)
# 출력: (1, 2, 3)
my_tuple = my_tuple + (4, 5, 6)
print(my_tuple)
# 출력: (1, 2, 3, 4, 5, 6)
딕셔너리 (Dictionary)
딕셔너리는 키-값(key-value) 쌍으로 데이터를 저장하는 변경 가능한 자료구조입니다. 자바스크립트의 객체(Object)와 유사합니다. 키는 불변 객체(문자열, 숫자, 튜플 등)여야 하며, 값은 어떤 타입이든 가능합니다.
person = {
'name': 'Josh',
'age': 33,
'height': "5'10",
'weight': '180 lbs'
}
딕셔너리는 변경 가능하므로 값을 수정할 수 있습니다.
person = {
'name': 'Josh',
'age': 33,
'height': "5'10",
'weight': '180 lbs'
}
person['name'] = 'Patrick'
print(person)
# 출력: {'name': 'Patrick', 'age': 33, 'height': "5'10", 'weight': '180 lbs'}
새로운 키-값 쌍을 추가하려면 존재하지 않는 키에 값을 할당하면 됩니다.
person = {
'name': 'Josh',
'age': 33,
'height': "5'10",
'weight': '180 lbs'
}
person['location'] = 'San Francisco'
print(person)
# 출력: {'name': 'Josh', 'age': 33, 'height': "5'10", 'weight': '180 lbs', 'location': 'San Francisco'}
키-값 쌍을 삭제하는 방법은 세 가지가 있습니다.
- del 문: 지정한 키를 삭제합니다.
- pop(key): 지정한 키의 값을 반환하며 삭제합니다. 키가 없으면 KeyError 발생.
- popitem(): 마지막에 추가된 키-값 쌍을 삭제하고 반환합니다. (파이썬 3.7+에서 삽입 순서 보장)
# del 사용
person = {'name': 'Josh', 'age': 33, 'height': "5'10", 'weight': '180 lbs'}
del person['name']
print(person)
# 출력: {'age': 33, 'height': "5'10", 'weight': '180 lbs'}
# pop() 사용
person = {'name': 'Josh', 'age': 33, 'height': "5'10", 'weight': '180 lbs'}
person.pop('name')
print(person)
# 출력: {'age': 33, 'height': "5'10", 'weight': '180 lbs'}
# popitem() 사용
person = {'name': 'Josh', 'age': 33, 'height': "5'10", 'weight': '180 lbs'}
person.popitem()
print(person)
# 출력: {'name': 'Josh', 'age': 33, 'height': "5'10"}
키만, 값만, 또는 키-값 쌍 전체를 조회할 수도 있습니다.
person = {'name': 'Josh', 'age': 33, 'height': "5'10", 'weight': '180 lbs'}
print(person.keys())
# 출력: dict_keys(['name', 'age', 'height', 'weight'])
print(person.values())
# 출력: dict_values(['Josh', 33, "5'10", '180 lbs'])
print(person.items())
# 출력: dict_items([('name', 'Josh'), ('age', 33), ('height', "5'10"), ('weight', '180 lbs')])
셋 (Set)
셋은 중복을 허용하지 않는 변경 가능한 비순서 컬렉션입니다. 중괄호 {}를 사용해 딕셔너리와 비슷해 보이지만, 키-값 쌍이 아닌 단일 값들만 저장합니다. 수학의 집합 개념을 구현한 것으로, 합집합, 교집합, 차집합 등의 연산이 가능합니다.
my_set = {1, 2, 2, 2, 3, 3, 4, 4}
print(my_set)
# 출력: {1, 2, 3, 4}
# 중복된 값은 자동으로 제거됨
요소 추가는 add() 메서드를 사용합니다.
my_set = {1, 2, 2, 2, 3, 3, 4, 4}
my_set.add(5)
print(my_set)
# 출력: {1, 2, 3, 4, 5}
셋 연산을 위한 주요 메서드 네 가지가 있습니다.
- union() (
|): 두 셋의 합집합을 반환합니다. 중복 없이 모든 요소를 포함합니다.
set_a = {1, 2, 3, 4, 5}
set_b = {3, 4, 5, 5, 6}
print(set_a.union(set_b))
# 출력: {1, 2, 3, 4, 5, 6}
- intersection() (
&): 두 셋의 교집합(공통 요소)을 반환합니다.
set_a = {1, 2, 3, 4, 5}
set_b = {3, 4, 5, 5, 6}
print(set_a.intersection(set_b))
# 출력: {3, 4, 5}
- difference() (
-): 첫 번째 셋에서 두 번째 셋과 공통되지 않는 요소만 반환합니다. (A - B)
set_a = {1, 2, 3, 4, 5}
set_b = {3, 4, 5, 5, 6}
print(set_a.difference(set_b))
# 출력: {1, 2}
- symmetric_difference() (
^): 두 셋 중 어느 한쪽에만 속하는 요소들(대칭 차집합)을 반환합니다.
set_a = {1, 2, 3, 4, 5}
set_b = {3, 4, 5, 5, 6}
print(set_a.symmetric_difference(set_b))
# 출력: {1, 2, 6}
요약: 언제 어떤 자료구조를 쓸까?
| 자료구조 | 변경 가능 | 순서 보장 | 중복 허용 | 주요 용도 |
|---|---|---|---|---|
| 리스트 (List) | O | O | O | 순서가 있는 데이터 시퀀스, 스택/큐 구현 |
| 튜플 (Tuple) | X | O | O | 불변 데이터, 딕셔너리 키, 다중 반환값 |
| 딕셔너리 (Dict) | O | O (3.7+) | 키: X, 값: O | 키-값 매핑, 빠른 조회, 객체 표현 |
| 셋 (Set) | O | X | X | 중복 제거, 집합 연산, 멤버십 테스트 |
각 자료구조의 특성을 이해하고 상황에 맞게 선택하는 것이 효율적인 파이썬 프로그래밍의 첫걸음입니다.