자료구조는 데이터를 저장하고 조직화하는 방식을 정의한다. Python(파이썬)은 리스트, 튜플, 집합, 사전 같은 내장 자료구조와, 이를 확장한 collections 모듈을 함께 제공해 대부분의 작업을 표준 라이브러리만으로 처리할 수 있다. 이 글에서는 각 자료구조의 특징과 시간복잡도, 실전에서 자주 쓰는 패턴을 정리한다.
리스트 (List)
리스트는 순서가 있고 변경 가능한(mutable) 자료구조다. 인덱스 접근과 끝에 원소를 추가하는 연산은 O(1)이지만, 중간에 삽입/삭제하면 뒤쪽 원소를 모두 옮겨야 해서 O(n)이 걸린다.
fruits = ['apple', 'banana', 'cherry']
print(fruits)
fruits.append('date')
print(fruits)
squares = [x**2 for x in range(5)]
print(squares)['apple', 'banana', 'cherry']
['apple', 'banana', 'cherry', 'date']
[0, 1, 4, 9, 16]
세 번째 줄처럼 [식 for 변수 in 반복가능객체] 형태로 쓰는 리스트 컴프리헨션(comprehension)은 for 루프 + append보다 빠르고 읽기 쉬워서 실무에서 널리 쓴다. 리스트에 대한 자세한 내용은 다음 글을 참조한다.
튜플 (Tuple)
튜플은 순서가 있지만 변경 불가능한(immutable) 자료구조다. 한번 생성되면 원소를 바꿀 수 없기 때문에 dict의 key나 set의 원소로 쓸 수 있고, 함수에서 여러 값을 묶어 반환할 때도 자주 쓴다.
t = (1, 2, 3)
print(t + t, t * 2)
print(len(t))
# 함수가 여러 값을 반환할 때 언패킹
a, b = 1, 2
a, b = b, a
print(a, b)(1, 2, 3, 1, 2, 3) (1, 2, 3, 1, 2, 3)
3
2 1
튜플에 대한 자세한 내용은 다음 글을 참조한다.
집합 (Set)
집합은 중복을 허용하지 않는 변경 가능한 자료구조다. 내부적으로 해시 테이블을 쓰기 때문에 원소 존재 여부 확인(in)이 리스트는 O(n)인 반면 집합은 평균 O(1)이다. 대량 데이터에서 중복 제거나 멤버십 체크가 필요하면 리스트 대신 집합을 쓰는 게 성능상 유리하다.
s1 = {'red', 'green', 'blue'}
s2 = {'green', 'yellow'}
print(s1 | s2) # 합집합
print(s1 & s2) # 교집합
print(s1 - s2) # 차집합
print(s1 ^ s2) # 대칭차집합(둘 중 하나에만 있는 원소){'blue', 'yellow', 'green', 'red'}
{'green'}
{'blue', 'red'}
{'blue', 'red', 'yellow'}
집합은 해시 기반이라 출력 순서가 삽입 순서와 다를 수 있다. 순서가 중요한 경우에는 리스트나 collections.OrderedDict를 쓴다.
사전 (Dictionary)
사전은 키-값(key-value) 쌍을 저장하는 변경 가능한 자료구조로, 집합과 마찬가지로 해시 테이블 기반이라 키로 값을 찾는 연산이 평균 O(1)이다. Python 3.7부터는 삽입 순서를 보장한다.
person = {'name': 'John', 'age': 30}
# 없는 키를 조회해도 예외 대신 기본값 반환
print(person.get('email', '없음'))
# 3.9+ : | 연산자로 딕셔너리 병합
person2 = {'age': 31, 'city': 'Seoul'}
merged = person | person2
print(merged)
# 딕셔너리 컴프리헨션
squared = {k: v**2 for k, v in {'a': 1, 'b': 2}.items()}
print(squared)없음
{'name': 'John', 'age': 31, 'city': 'Seoul'}
{'a': 1, 'b': 4}
문자열 (String)
문자열은 변경 불가능한 자료구조로, 문자의 시퀀스를 나타낸다. 슬라이싱과 f-string, 각종 메서드를 조합해서 다루는 경우가 대부분이다.
text = "Hello, World!"
print(text[:5], text.upper(), text.split(','))
name = "Junorion"
print(f"안녕하세요, {name}님")Hello HELLO, WORLD! ['Hello', ' World!']
안녕하세요, Junorion님
배열 (Array)
배열은 순서가 있는 변경 가능한 자료구조로, 리스트와 달리 원소가 모두 동일한 데이터 타입이어야 한다. 그만큼 메모리를 덜 쓰기 때문에 대량의 숫자 데이터를 다룰 때 유리하다. 표준 라이브러리 array 모듈을 가져와야 쓸 수 있다.
from array import array
numbers = array('i', [1, 2, 3, 4, 5])
print(numbers)array('i', [1, 2, 3, 4, 5])
숫자 배열을 대량으로 다루면서 벡터 연산까지 필요하면 array 모듈보다는 NumPy의 ndarray를 쓰는 편이 실용적이다.
collections 모듈
기본 자료구조만으로 부족한 경우를 위해 표준 라이브러리 collections 모듈이 확장 자료구조를 제공한다. 실무에서 특히 자주 쓰는 네 가지는 다음과 같다.
deque— 양쪽 끝에서 추가/삭제가 O(1)인 큐. 리스트는 맨 앞에 삽입/삭제하면 O(n)이라 큐로 쓰기엔 비효율적이다.Counter— 원소별 등장 횟수를 세는 dict 서브클래스. 빈도수 집계에 쓴다.defaultdict— 없는 키에 접근하면 지정한 타입의 기본값으로 자동 초기화한다.if key not in d체크를 생략할 수 있다.namedtuple— 필드에 이름을 붙인 튜플. 인덱스 대신 속성명으로 접근할 수 있어 가독성이 좋다.
from collections import deque, Counter, defaultdict, namedtuple
q = deque([1, 2, 3])
q.appendleft(0)
q.append(4)
print(q)
q.popleft()
print(q)
c = Counter("abracadabra")
print(c)
print(c.most_common(2))
dd = defaultdict(list)
dd['fruits'].append('apple')
dd['fruits'].append('banana')
print(dict(dd))
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(p, p.x, p.y)deque([0, 1, 2, 3, 4])
deque([1, 2, 3, 4])
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
[('a', 5), ('b', 2)]
{'fruits': ['apple', 'banana']}
Point(x=1, y=2) 1 2
자료구조 선택 기준
어떤 자료구조를 쓸지는 결국 “무엇을 자주 하느냐”로 결정한다.
- 순서가 중요하고 값이 자주 바뀐다 → 리스트
- 값이 절대 바뀌면 안 되거나 dict의 key/set의 원소로 써야 한다 → 튜플
- 중복 제거, 빠른 존재 여부 확인, 집합 연산이 필요하다 → 집합
- 키로 값을 빠르게 찾아야 한다 → 사전
- 양쪽 끝에서 추가/삭제가 잦은 큐가 필요하다 →
collections.deque - 같은 타입의 숫자를 대량으로 저장하며 메모리를 아껴야 한다 →
array(단순 저장) 또는 NumPy(연산까지 필요할 때)