컬렉션과 컴프리헨션
list/tuple/dict/set 네 컬렉션의 쓰임새, 그리고 한 줄로 새 컬렉션을 만드는 컴프리헨션과 제너레이터 표현식까지 정리합니다.
3장 제어 흐름 마지막에 살짝 본 컴프리헨션을 본격적으로 다룰 차례입니다. 그 전에 파이썬의 네 가지 핵심 컬렉션 — list, tuple, dict, set의 쓰임새부터 정리합니다.
본 챕터의 두 축은 (1) 네 컬렉션의 차이를 정확히 아는 것, (2) 컴프리헨션 한 줄에 익숙해지는 것입니다. 두 축 모두 책 전체에서 매일 만나는 도구입니다. 11장 이터러블, 제너레이터, yield from에서는 본 챕터의 컴프리헨션과 제너레이터 표현식이 어떻게 같은 이터레이터 프로토콜 위에 올라가는지 깊이 다룹니다.
한 표로 보는 비교 #
| 변경 가능 | 순서 | 중복 | 키-값 | 표기 | |
|---|---|---|---|---|---|
list | O | O | 허용 | X | [1, 2, 3] |
tuple | X | O | 허용 | X | (1, 2, 3) |
set | O | X | 불허 | X | {1, 2, 3} |
dict | O | O¹ | 키 unique | O | {"a": 1} |
¹ Python 3.7+ 부터 dict는 삽입 순서를 보장합니다.
list — 가장 많이 쓰는 컬렉션
#
nums: list[int] = [1, 2, 3]
nums.append(4) # [1, 2, 3, 4]
nums.insert(0, 0) # [0, 1, 2, 3, 4]
nums.remove(2) # [0, 1, 3, 4]
last = nums.pop() # 4 반환, 리스트는 [0, 1, 3]
print(len(nums)) # 3
print(nums[0]) # 0
print(3 in nums) # True슬라이스 — [start:stop:step]
#
리스트는 슬라이스와 함께 쓸 때 특히 유용합니다.
items = [10, 20, 30, 40, 50]
items[1:3] # [20, 30] 1 이상 3 미만
items[:2] # [10, 20] 처음부터 2 미만
items[3:] # [40, 50] 3 이상부터 끝까지
items[-2:] # [40, 50] 끝에서 2개
items[::2] # [10, 30, 50] step 2
items[::-1] # [50, 40, 30, 20, 10] 뒤집기step = -1로 리스트를 뒤집는 패턴은 자주 등장합니다. JavaScript의 arr.toReversed()보다 짧고 빠릅니다.
슬라이스는 새 리스트를 만들어 반환 합니다. 원본은 안 바뀝니다.
+와 * — 결합과 반복
#
[1, 2] + [3, 4] # [1, 2, 3, 4]
[0] * 5 # [0, 0, 0, 0, 0][0] * 5는 0으로 채운 리스트를 만드는 가장 짧은 방법입니다. 다만 참조 타입을 곱하면 모두 같은 객체가 됩니다.
matrix = [[]] * 3
matrix[0].append(1)
print(matrix)
# [[1], [1], [1]] ← 모두 같은 리스트를 가리킴!이런 경우는 컴프리헨션을 써야 합니다 (아래에서 다룸).
tuple — 모양이 정해진 묶음
#
list와 거의 같은데 변경 불가입니다. 그 대신 쓰임새가 다릅니다.
point: tuple[float, float] = (1.0, 2.0)
person: tuple[str, int] = ("커티스", 30)
# tuple 은 보통 unpack 으로 풀어서 씀
name, age = person
print(name, age) # 커티스 30
# 한 원소 tuple 은 콤마가 필수 — 괄호만으론 부족
single = (42,) # tuple
not_tuple = (42) # 그냥 정수tuple이 어울리는 경우 #
- 여러 값을 함께 반환 할 때:
return name, age(사실은 tuple) - 딕셔너리 키: list는 키로 못 쓰지만 tuple은 됨 (불변이라)
- 모양이 정해진 좌표 / 날짜 같은 데이터 — list보다 의도가 분명
더 명시적인 tuple — NamedTuple
#
위치가 정해진 tuple은 시간이 지나면 헷갈립니다 (person[0]이 이름이었던가?). 이름을 붙인 tuple을 쓰면 깔끔합니다.
from typing import NamedTuple
class Person(NamedTuple):
name: str
age: int
p = Person("커티스", 30)
print(p.name, p.age) # 커티스 30
# tuple 처럼 unpack 도 됨
name, age = pNamedTuple보다 더 일반적으로 쓰이는 @dataclass는 8장 dataclass와 __slots__에서 본격적으로 다룹니다. 변경 가능한 데이터, 메소드 정의, 검증 등이 필요하면 dataclass 쪽이 더 자연스럽습니다.
dict — 키-값 매핑
#
user: dict[str, int] = {"id": 1, "age": 30}
print(user["id"]) # 1
print(user.get("nope")) # None (없는 키 → 안전)
print(user.get("nope", -1)) # -1 (기본값)
user["name"] = "curtis" # 추가/수정
del user["age"] # 삭제
print("name" in user) # Trueuser["없는키"]는 KeyError 예외를 던집니다. 안전하게 꺼내려면 .get()을 쓰세요.
순회 #
for key in user: # 키만
print(key)
for key, value in user.items(): # 둘 다
print(key, value)
for value in user.values(): # 값만
print(value).items()는 거의 매일 씁니다.
합치기 — | 연산자 (3.9+)
#
defaults = {"a": 1, "b": 2}
overrides = {"b": 20, "c": 30}
merged = defaults | overrides
print(merged) # {"a": 1, "b": 20, "c": 30}|는 오른쪽이 이김 — 같은 키면 뒤쪽 값이 살아남습니다. JavaScript의 {...defaults, ...overrides}와 같은 의미.
set — 중복 없는 묶음
#
unique: set[int] = {1, 2, 3, 2, 1}
print(unique) # {1, 2, 3}
unique.add(4) # {1, 2, 3, 4}
unique.discard(2) # {1, 3, 4}
print(3 in unique) # True
# 빈 set 은 set() — {} 는 빈 dict
empty = set()집합 연산 #
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a | b # 합집합 {1, 2, 3, 4, 5, 6}
a & b # 교집합 {3, 4}
a - b # 차집합 {1, 2}
a ^ b # 대칭차 {1, 2, 5, 6}리스트 중복 제거의 가장 짧은 방법: list(set(items)). 다만 순서가 깨질 수 있습니다. 순서를 유지하면서 중복 제거하려면:
items = [1, 2, 1, 3, 2, 4]
unique = list(dict.fromkeys(items))
print(unique) # [1, 2, 3, 4]dict.fromkeys()가 dict의 순서 보존 + 키 unique 성질을 동시에 활용합니다.
컴프리헨션 — 한 줄로 컬렉션 만들기 #
for 루프와 조건을 한 줄에 합치는 문법입니다. 파이썬에서 가장 자주 쓰는 패턴 중 하나입니다.
리스트 컴프리헨션 #
# [표현식 for 변수 in 이터러블]
squares = [x ** 2 for x in range(5)]
# [0, 1, 4, 9, 16]for 루프로 풀어 쓰면:
squares = []
for x in range(5):
squares.append(x ** 2)다섯 줄짜리가 한 줄로 줄어듭니다. 읽기도 더 빨라요 — 한 번에 의도가 보입니다.
조건 — 필터 #
evens = [x for x in range(10) if x % 2 == 0]
# [0, 2, 4, 6, 8]if는 필터입니다. 조건에 맞는 원소만 들어갑니다.
변환 + 필터 동시 #
# 짝수만 골라서 제곱
result = [x ** 2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]if-else 표현식 (위치에 주의)
#
if-else는 표현식 위치에 들어갑니다. 필터의 if와 위치가 다릅니다.
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
# ['even', 'odd', 'even', 'odd', 'even']순서는 [표현식 if cond else 다른표현식 for x in iter]. 헷갈리는 부분이니 천천히 읽으세요.
중첩 — 2D 만들기 #
matrix = [[0 for _ in range(3)] for _ in range(3)]
# [[0, 0, 0], [0, 0, 0], [0, 0, 0]]앞에서 본 [[0]] * 3의 함정을 컴프리헨션이 해결합니다. 매번 새 리스트가 만들어지니 서로 독립입니다.
딕셔너리 컴프리헨션 #
square_map = {x: x ** 2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# 키-값 swap
user = {"id": 1, "name": "curtis"}
swapped = {v: k for k, v in user.items()}
# {1: "id", "curtis": "name"}세트 컴프리헨션 #
unique_lengths = {len(w) for w in ["a", "bb", "cc", "ddd"]}
# {1, 2, 3}함정 — 중첩 for의 순서 #
pairs = [(x, y) for x in [1, 2] for y in ['a', 'b']]
# [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]여러 for가 있을 때 왼쪽이 바깥 루프입니다. 풀어 쓰면:
pairs = []
for x in [1, 2]:
for y in ['a', 'b']:
pairs.append((x, y))제너레이터 표현식 — 메모리를 아낀다 #
대괄호 대신 소괄호를 쓰면 컴프리헨션이 아니라 제너레이터가 됩니다.
gen = (x ** 2 for x in range(1_000_000))
print(gen) # <generator object ...>리스트 컴프리헨션은 즉시 모든 원소를 만듭니다. 100만 개면 100만 개분의 메모리를 잡습니다. 제너레이터는 요청할 때만 다음 값을 만들기 때문에 메모리가 거의 안 듭니다.
total = sum(x ** 2 for x in range(1_000_000))
# 함수에 바로 넣으면 ()를 생략할 수 있음sum, max, any, all 같이 한 번 순회만 하면 되는 함수에 넘길 때가 제너레이터의 적합한 용도입니다.
| 리스트 컴프리헨션 | 제너레이터 식 | |
|---|---|---|
| 표기 | [ ... ] | ( ... ) |
| 메모리 | 모든 원소 즉시 생성 | 필요할 때만 |
| 재사용 | 여러 번 순회 가능 | 한 번만 순회 가능 |
| 인덱스 접근 | O result[3] | X |
제너레이터의 더 깊은 사용 — yield, yield from, async generator — 은 11장 이터러블, 제너레이터, yield from에서 다룹니다.
언제 컴프리헨션, 언제 풀어쓰기? #
컴프리헨션이 항상 좋은 건 아닙니다. 로직이 복잡해지면 풀어쓰는 게 더 읽기 좋습니다.
# 한 줄로 가능은 한데, 읽기 어려움
result = [transform(x) for x in items if validate(x) and is_active(x) and not is_deleted(x)]
# 풀어 쓰면 디버그하기 좋고 읽기도 좋음
result = []
for x in items:
if not validate(x):
continue
if not is_active(x) or is_deleted(x):
continue
result.append(transform(x))판단 기준은: 한 줄에 한 표현식만, if는 한 개까지 정도가 컴프리헨션이 빛나는 범위입니다. 그 이상은 풀어쓰는 게 보통 낫습니다.
연습문제 #
users: list[dict[str, int | str]]가[{"name": "a", "age": 30}, {"name": "b", "age": 17}, {"name": "c", "age": 22}]형태로 주어집니다. 컴프리헨션 한 줄로 성인(19 이상)의 이름만 골라 list 로 반환하는 표현식을 작성하세요.words = ["apple", "banana", "cherry", "date"]에서 dict 컴프리헨션으로{"apple": 5, "banana": 6, "cherry": 6, "date": 4}같은 단어 → 길이 매핑을 만드세요.range(1, 100_000_001)의 짝수 제곱의 합을 구해야 합니다. (1) 리스트 컴프리헨션sum([x**2 for x in range(...) if x % 2 == 0])로 한 번, (2) 제너레이터 식sum(x**2 for x in range(...) if x % 2 == 0)로 한 번 구해 보세요. 메모리 / 시간 차이를 직접 체감합니다 (메모리 모니터링은 21장에서 다시 다룹니다).
한 줄 요약: 네 컬렉션의 선택은 변경가능성 / 순서 / 중복 / 키-값 네 축으로 갈린다.
list슬라이스,dict.get과.items(),set집합 연산이 일상의 90%. 컴프리헨션 한 줄이 보통 더 짧고 빠르지만, 조건 / 변환이 복잡해지면 풀어쓰는 게 낫다. 메모리가 부담되는 한 번 순회는 제너레이터 식(...).
다음 챕터 #
다음 5장 함수 — 인자 패턴에서는 함수 정의의 다양한 인자 패턴을 다룹니다. 위치 / 키워드 / 기본값 / *args / **kwargs / 위치 전용 / 키워드 전용까지입니다. 본 챕터의 컴프리헨션과 합쳐지면 함수형 스타일의 데이터 변환 코드를 짧게 적을 수 있게 됩니다.