목차
4 장

컬렉션과 컴프리헨션

list/tuple/dict/set 네 컬렉션의 쓰임새, 그리고 한 줄로 새 컬렉션을 만드는 컴프리헨션과 제너레이터 표현식까지 정리합니다.

3장 제어 흐름 마지막에 살짝 본 컴프리헨션을 본격적으로 다룰 차례입니다. 그 전에 파이썬의 네 가지 핵심 컬렉션list, tuple, dict, set의 쓰임새부터 정리합니다.

본 챕터의 두 축은 (1) 네 컬렉션의 차이를 정확히 아는 것, (2) 컴프리헨션 한 줄에 익숙해지는 것입니다. 두 축 모두 책 전체에서 매일 만나는 도구입니다. 11장 이터러블, 제너레이터, yield from에서는 본 챕터의 컴프리헨션과 제너레이터 표현식이 어떻게 같은 이터레이터 프로토콜 위에 올라가는지 깊이 다룹니다.

한 표로 보는 비교 #

변경 가능순서중복키-값표기
listOO허용X[1, 2, 3]
tupleXO허용X(1, 2, 3)
setOX불허X{1, 2, 3}
dictO키 uniqueO{"a": 1}

¹ Python 3.7+ 부터 dict는 삽입 순서를 보장합니다.

list — 가장 많이 쓰는 컬렉션 #

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와 거의 같은데 변경 불가입니다. 그 대신 쓰임새가 다릅니다.

tuple 기본
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을 쓰면 깔끔합니다.

NamedTuple
from typing import NamedTuple

class Person(NamedTuple):
    name: str
    age: int

p = Person("커티스", 30)
print(p.name, p.age)   # 커티스 30

# tuple 처럼 unpack 도 됨
name, age = p

NamedTuple보다 더 일반적으로 쓰이는 @dataclass는 8장 dataclass와 __slots__에서 본격적으로 다룹니다. 변경 가능한 데이터, 메소드 정의, 검증 등이 필요하면 dataclass 쪽이 더 자연스럽습니다.

dict — 키-값 매핑 #

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)          # True

user["없는키"]KeyError 예외를 던집니다. 안전하게 꺼내려면 .get()을 쓰세요.

순회 #

dict 순회
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+) #

dict 합치기
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 — 중복 없는 묶음 #

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)

다섯 줄짜리가 한 줄로 줄어듭니다. 읽기도 더 빨라요 — 한 번에 의도가 보입니다.

조건 — 필터 #

if 절
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 만들기 #

중첩 컴프리헨션 — 2D 매트릭스
matrix = [[0 for _ in range(3)] for _ in range(3)]
# [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

앞에서 본 [[0]] * 3의 함정을 컴프리헨션이 해결합니다. 매번 새 리스트가 만들어지니 서로 독립입니다.

딕셔너리 컴프리헨션 #

dict comprehension
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"}

세트 컴프리헨션 #

set comprehension
unique_lengths = {len(w) for w in ["a", "bb", "cc", "ddd"]}
# {1, 2, 3}

함정 — 중첩 for의 순서 #

중첩 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는 한 개까지 정도가 컴프리헨션이 빛나는 범위입니다. 그 이상은 풀어쓰는 게 보통 낫습니다.

연습문제 #

  1. users: list[dict[str, int | str]][{"name": "a", "age": 30}, {"name": "b", "age": 17}, {"name": "c", "age": 22}] 형태로 주어집니다. 컴프리헨션 한 줄로 성인(19 이상)의 이름만 골라 list 로 반환하는 표현식을 작성하세요.
  2. words = ["apple", "banana", "cherry", "date"]에서 dict 컴프리헨션으로 {"apple": 5, "banana": 6, "cherry": 6, "date": 4} 같은 단어 → 길이 매핑을 만드세요.
  3. 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 / 위치 전용 / 키워드 전용까지입니다. 본 챕터의 컴프리헨션과 합쳐지면 함수형 스타일의 데이터 변환 코드를 짧게 적을 수 있게 됩니다.

X