[Python] Comprehension: list, dict, set, generator
정의
**Comprehension(컴프리헨션)**은 iterable로부터 list/set/dict/generator를 선언적·간결하게 만드는 문법이다. 일반 for+append 보다 30-50% 빠르며 (전용 바이트코드 LIST_APPEND 등), Pythonic 코드의 핵심 관용구다.
4가지 형태
[expr for x in iter if cond] # list
{expr for x in iter if cond} # set
{k: v for x in iter if cond} # dict
(expr for x in iter if cond) # generator (괄호: lazy)
평가 순서 시각화
flowchart LR
I["iterable 소스"] --> F["for x in iter"]
F -->|"cond 있으면"| C["if cond (필터)"]
F -->|"cond 없으면"| E["expr 계산"]
C -->|"True"| E
C -->|"False"| F
E --> R["결과 컬렉션에 추가"]
R --> F
중첩 for 는 왼쪽이 바깥 루프다.
flowchart LR
A["for x in A (바깥)"] --> B["for y in B (안쪽)"]
B --> E["(x, y) 추가"]
E --> B
B --> A
list comprehension
squares = [x ** 2 for x in range(5)]
print(squares)
evens = [x for x in range(10) if x % 2 == 0]
print(evens)
# 변환 + 필터 동시
upper_long = [s.upper() for s in ["hi", "world", "py"] if len(s) > 2]
print(upper_long)
# 중첩 for
pairs = [(x, y) for x in range(2) for y in range(2)]
print(pairs)
# 2차원 → 1차원 (flatten)
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [x for row in matrix for x in row]
print(flat)[0, 1, 4, 9, 16]
[0, 2, 4, 6, 8]
['WORLD']
[(0, 0), (0, 1), (1, 0), (1, 1)]
[1, 2, 3, 4, 5, 6]중첩 for 순서
[(x, y) for x in A for y in B]
# 동등한 일반 for
result = []
for x in A:
for y in B:
result.append((x, y))
왼쪽 for가 바깥 루프. 헷갈리면 일반 for로 바꿔서 확인.
조건부 표현식
if는 필터, if-else는 표현식 변환.
# 필터 (수만 통과)
[x for x in nums if x > 0]
# 변환 (모두 통과, 값만 바뀜)
[x if x > 0 else 0 for x in nums]
dict comprehension
squares = {x: x ** 2 for x in range(5)}
print(squares)
# dict 뒤집기
d = {"a": 1, "b": 2}
inverted = {v: k for k, v in d.items()}
print(inverted)
# 필터링
filtered = {k: v for k, v in d.items() if v > 1}
print(filtered){0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
{1: 'a', 2: 'b'}
{'b': 2}set comprehension
unique_lengths = {len(s) for s in ["a", "bb", "cc", "ddd"]}
print(unique_lengths)
vowels = {ch for ch in "comprehensions" if ch in "aeiou"}
print(vowels){1, 2, 3}
{'e', 'i', 'o'}generator expression
**괄호()**로 감싸면 generator를 반환한다. 메모리 효율적인 lazy 시퀀스.
# 차이: list (즉시 생성) vs gen (lazy)
import sys
lst = [x ** 2 for x in range(1000)]
gen = (x ** 2 for x in range(1000))
print(sys.getsizeof(lst), "bytes (list)")
print(sys.getsizeof(gen), "bytes (gen)")
# 함수 단일 인자일 때 괄호 생략 가능
total = sum(x ** 2 for x in range(100))
print(total)8056 bytes (list)
208 bytes (gen)
328350거대한 데이터를 처리할 때는 항상 generator 먼저 고려. list 컴프리헨션을 메모리에 들고 있을 이유가 없으면 괄호로 바꿔라.
성능: list comp vs for loop vs map
| 방법 | CPython 속도 | 메모리 |
|---|---|---|
| list comprehension | 기준 (1.0x) | 즉시 |
| 일반 for + append | ~1.3-1.5x 느림 | 즉시 |
map() + lambda | ~1.1-1.2x 느림 | lazy |
map() + 내장 함수 | ~0.9x (약간 빠름) | lazy |
| generator expression | 기준 (1.0x) | lazy (훨씬 적음) |
# list comp
[x ** 2 for x in xs if x > 0]
# map + filter (역사적 대안)
list(map(lambda x: x ** 2, filter(lambda x: x > 0, xs)))
CPython에서 lambda 호출 비용이 크기 때문에 list comp가 거의 항상 빠르다. map(operator.add, ...)처럼 lambda 없이 내장 함수만 쓰면 map이 비등하거나 더 빠를 수 있다.
Walrus(:=)와 결합
같은 비싼 계산을 필터와 표현식에서 두 번 하지 않게.
# WRONG: f(x)를 두 번 호출
results = [f(x) for x in xs if f(x) is not None]
# 3.8+
results = [y for x in xs if (y := f(x)) is not None]
async comprehension (3.6+)
async def fetch_all():
return [await fetch(url) async for url in urls()]
# async generator
async def squares():
async for n in numbers():
yield n ** 2
async 컨텍스트(코루틴 함수 내부)에서만 가능.
실전 패턴
JSON 필드 추출
users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
names = [u["name"] for u in users if u["id"] > 0]
id_map = {u["id"]: u["name"] for u in users}
중복 제거 (순서 유지)
seen = set()
unique = [x for x in items if not (x in seen or seen.add(x))]
set.add() 가 None 반환하므로 or seen.add(x) 트릭으로 순서 유지 dedup.
중첩 dict 평탄화
data = {"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}}
flat = {f"{outer}_{inner}": v
for outer, inner_d in data.items()
for inner, v in inner_d.items()}
# {'a_x': 1, 'a_y': 2, 'b_x': 3, 'b_y': 4}
그룹핑 (itertools 없이)
from collections import defaultdict
words = ["apple", "banana", "avocado", "cherry", "blueberry"]
by_first = defaultdict(list)
for w in words:
by_first[w[0]].append(w)
# 컴프리헨션으로 표현 (가독성 주의)
by_first2 = {k: [w for w in words if w[0] == k]
for k in {w[0] for w in words}}
첫 번째 방식이 O(n), 두 번째가 O(n^2)이다. 가독성을 위해 복잡도 희생하지 말 것.
가독성 한계
너무 복잡하면 일반 for로 바꿔라. 일반적 규칙:
- 중첩 3단계 이상 → for로 분해
- 조건 2개 이상 + 변환 복잡 → 함수로 추출
- 한 줄 80자 초과 → 부분식 변수로 추출 또는 for
# 나쁨: 한 줄에 너무 많음
result = [transform(x) for sublist in matrix for x in sublist if x > 0 and not is_blacklisted(x) and ...]
# 좋음
def is_valid(x):
return x > 0 and not is_blacklisted(x)
result = [transform(x)
for sublist in matrix
for x in sublist
if is_valid(x)]
스코프 (3.x)
Python 3 컴프리헨션은 자체 스코프를 가진다. 변수 누출 없음.
x = 99
squares = [x ** 2 for x in range(5)]
print(x) # 99 (덮어쓰지 않음)
Python 2에서는 누출됐지만 3에서 수정됨.
함정
WARNING
비싼 호출 반복: 필터와 표현식에서 같은 함수를 두 번 호출하지 말 것.
# WRONG: 매 반복마다 expensive_call() 두 번 호출
[expensive_call(x) for x in xs if expensive_call(x).is_ok]
# OK: walrus operator 활용
[y for x in xs if (y := expensive_call(x)).is_ok]
WARNING
generator를 두 번 소비: generator expression은 한 번만 소비 가능.
gen = (x ** 2 for x in range(5))
list(gen) # [0, 1, 4, 9, 16]
list(gen) # [] (소진됨)
WARNING
순회 중 변경: dict/set comprehension 내부에서 소스를 변경하면 안 됨.
관련 위키
- py-iterator-generator - generator 심층: yield, send, throw
- py-list - list 자체: 메모리 레이아웃, 시간복잡도
- py-dict - dict comprehension 의 타겟 자료구조
- py-functools -
reduce,partial등 함수형 도구 - py-asyncio - async comprehension 의 기반
이 글의 용어 (5개)
- [Python] asyncio: 이벤트 루프, async/awaitpython
- 정의 는 Python 표준 라이브러리의 단일 스레드 이벤트 루프 기반 비동기 I/O 프레임워크다. 로 코루틴을 정의하고 로 다른 코루틴/awaitable 완료를 기다린다. I/O…
- [Python] dict: 해시 맵과 삽입 순서 보장python
- 정의 는 Python의 해시 맵 자료구조다. Python 3.7부터 삽입 순서를 언어 사양으로 보장한다(이전엔 CPython 3.6 구현 디테일이었음). 평균 O(1) 조회·삽입…
- [Python] functools: cache, partial, reduce, singledispatchpython
- 정의 는 함수형 프로그래밍 도구와 함수 변환 데코레이터를 제공하는 표준 라이브러리. 메모이제이션, 부분 적용, 디스패치, 데코레이터 메타데이터 등을 다룬다. lrucache / …
- [Python] Iterator, Generator, yieldpython
- Iterator Protocol Iterator는 와 를 구현한 객체. Iterable은 만 가지면 된다 (iterator를 반환). 문은 내부적으로 로 iterator를 얻고 …
- [Python] list: 가변 시퀀스python
- 정의 Python의 는 가변 길이 동적 배열(dynamic array)이다. C의 /Java 와 같은 자료구조로, 내부적으로 PyObject 포인터의 연속 배열을 over-all…
💬 댓글