4.4 컴프리헨션 완전 정복 — list, dict, set, generator
컴프리헨션(Comprehension)은 Python의 가장 강력하고 Python다운 문법 중 하나입니다. 반복문과 조건문을 단 한 줄로 압축하여 표현하며, 올바르게 사용하면 코드를 더 읽기 쉽고 빠르게 만들 수 있습니다.
리스트 컴프리헨션
# 기본 문법: [표현식 for 변수 in 이터러블]
squares = [x**2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# 조건 필터: [표현식 for 변수 in 이터러블 if 조건]
evens = [x for x in range(20) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# 기존 for 루프와 비교
# 루프 버전:
result = []
for x in range(10):
if x % 2 == 0:
result.append(x**2)
# 컴프리헨션 버전:
result = [x**2 for x in range(10) if x % 2 == 0]
print(result) # [0, 4, 16, 36, 64]
# 조건 표현식(삼항)과 결합
numbers = [-3, -1, 0, 2, 5, -7, 8]
abs_values = [x if x >= 0 else -x for x in numbers]
print(abs_values) # [3, 1, 0, 2, 5, 7, 8]
# 문자열 처리
words = ["Hello", "World", "Python", "Programming"]
upper_long = [w.upper() for w in words if len(w) > 5]
print(upper_long) # ['PYTHON', 'PROGRAMMING']
딕셔너리 컴프리헨션
# 기본 문법: {키: 값 for 변수 in 이터러블}
squares_dict = {x: x**2 for x in range(1, 6)}
print(squares_dict) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# 조건 필터
even_squares = {x: x**2 for x in range(1, 11) if x % 2 == 0}
print(even_squares) # {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}
# 기존 딕셔너리 변환
prices = {"apple": 1000, "banana": 500, "cherry": 2000}
# 값에 10% 할인 적용
discounted = {k: int(v * 0.9) for k, v in prices.items()}
print(discounted) # {'apple': 900, 'banana': 450, 'cherry': 1800}
# 키-값 반전
inverted = {v: k for k, v in prices.items()}
print(inverted) # {1000: 'apple', 500: 'banana', 2000: 'cherry'}
# 리스트에서 딕셔너리 생성
keys = ["name", "age", "city"]
values = ["Alice", 30, "Seoul"]
person = {k: v for k, v in zip(keys, values)}
print(person) # {'name': 'Alice', 'age': 30, 'city': 'Seoul'}
# 조건부 딕셔너리: 특정 키만 포함
full_data = {"name": "Alice", "age": 30, "password": "secret", "email": "a@b.com"}
public_fields = {"name", "age", "email"}
public_data = {k: v for k, v in full_data.items() if k in public_fields}
print(public_data) # {'name': 'Alice', 'age': 30, 'email': 'a@b.com'}
셋 컴프리헨션
# 기본 문법: {표현식 for 변수 in 이터러블}
unique_squares = {x**2 for x in range(-5, 6)}
print(sorted(unique_squares)) # [0, 1, 4, 9, 16, 25] — 중복 제거, 순서 없음
# 중복 제거에 활용
words = ["hello", "world", "Hello", "Python", "python", "HELLO"]
unique_lower = {w.lower() for w in words}
print(unique_lower) # {'hello', 'world', 'python'}
# 교집합, 합집합 계산
a = {1, 2, 3, 4, 5}
b = {3, 4, 5, 6, 7}
# 셋 연산
intersection = a & b # {3, 4, 5}
union = a | b # {1, 2, 3, 4, 5, 6, 7}
difference = a - b # {1, 2}
symmetric_diff = a ^ b # {1, 2, 6, 7}
# 컴프리헨션으로 조건부 셋
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_set = {n for n in numbers if n % 2 == 0}
print(even_set) # {2, 4, 6, 8, 10}
제너레이터 표현식: 지연 평가
# 기본 문법: (표현식 for 변수 in 이터러블)
# 괄호 사용, 즉시 계산하지 않고 필요할 때 하나씩 생성
# 리스트 컴프리헨션: 즉시 전체 리스트 생성 (메모리 O(n))
squares_list = [x**2 for x in range(1_000_000)]
print(type(squares_list)) # <class 'list'>
# 제너레이터 표현식: 이터레이터 객체 반환 (메모리 O(1))
squares_gen = (x**2 for x in range(1_000_000))
print(type(squares_gen)) # <class 'generator'>
# 값을 필요할 때만 계산
import sys
print(f"리스트 메모리: {sys.getsizeof(squares_list):,} bytes")
print(f"제너레이터 메모리: {sys.getsizeof(squares_gen)} bytes")
# 리스트: 8,448,728 bytes
# 제너레이터: 104 bytes (크기에 무관!)
# 제너레이터 사용 예: 대용량 파일 처리
def read_large_file(filepath: str):
"""메모리 효율적 파일 읽기"""
with open(filepath, "r", encoding="utf-8") as f:
# 제너레이터 표현식으로 필터링
non_empty = (line.strip() for line in f if line.strip())
for line in non_empty:
yield line
# sum(), max(), min()에 제너레이터 전달 (함수 호출에서 괄호 생략 가능)
total = sum(x**2 for x in range(100)) # 괄호 하나로도 가능
maximum = max(len(w) for w in ["hello", "world", "python"])
print(total) # 328350
print(maximum) # 6
# 제너레이터 체이닝
data = range(1, 11)
pipeline = (x**2 for x in data if x % 2 == 0) # 짝수의 제곱
filtered = (x for x in pipeline if x > 10) # 10보다 큰 것만
result = list(filtered)
print(result) # [16, 36, 64, 100]
중첩 컴프리헨션
# 2중 for 루프를 단일 컴프리헨션으로
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# 행렬 평탄화 (flatten)
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 읽는 순서: "각 row에 대해, 그 row의 각 num"
# for row in matrix:
# for num in row:
# flat.append(num)
# 2D 리스트 생성
grid = [[i * j for j in range(1, 5)] for i in range(1, 5)]
for row in grid:
print(row)
# [1, 2, 3, 4]
# [2, 4, 6, 8]
# [3, 6, 9, 12]
# [4, 8, 12, 16]
# 조건 포함 중첩
pairs = [(x, y) for x in range(1, 4) for y in range(1, 4) if x != y]
print(pairs) # [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
# 주의: 3중 이상은 가독성이 크게 떨어짐 — 루프로 분리 권장
# 나쁜 예:
flat_3d = [z for cube in [matrix] for row in cube for z in row]
# 좋은 예:
def flatten(nested):
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
컴프리헨션 vs 루프 성능 비교
import timeit
# 리스트 컴프리헨션
def test_comprehension():
return [x**2 for x in range(10000)]
# for 루프 + append
def test_loop():
result = []
for x in range(10000):
result.append(x**2)
return result
# map() 사용
def test_map():
return list(map(lambda x: x**2, range(10000)))
# 측정
n = 1000
t1 = timeit.timeit(test_comprehension, number=n)
t2 = timeit.timeit(test_loop, number=n)
t3 = timeit.timeit(test_map, number=n)
print(f"컴프리헨션: {t1:.3f}s")
print(f"루프: {t2:.3f}s")
print(f"map(): {t3:.3f}s")
# 일반적으로: 컴프리헨션 ≈ map() < 루프 (append 오버헤드)
실전 예제 1: 데이터 변환 파이프라인
from typing import TypedDict
class Student(TypedDict):
name: str
score: int
grade: str
raw_data = [
{"name": "Alice", "score": 92},
{"name": "Bob", "score": 78},
{"name": "Charlie", "score": 85},
{"name": "Diana", "score": 61},
{"name": "Eve", "score": 95},
]
def assign_grade(score: int) -> str:
return (
"A" if score >= 90 else
"B" if score >= 80 else
"C" if score >= 70 else
"D" if score >= 60 else "F"
)
# 변환 + 필터 + 정렬 파이프라인
processed: list[Student] = sorted(
[
{"name": d["name"], "score": d["score"], "grade": assign_grade(d["score"])}
for d in raw_data
if d["score"] >= 70 # 70점 이상만
],
key=lambda s: s["score"],
reverse=True
)
for student in processed:
print(f" {student['name']}: {student['score']}점 ({student['grade']})")
# Eve: 95점 (A)
# Alice: 92점 (A)
# Charlie: 85점 (B)
# Bob: 78점 (C)
실전 예제 2: 데이터 집계
from collections import defaultdict
transactions = [
{"category": "food", "amount": 12000},
{"category": "transport", "amount": 3500},
{"category": "food", "amount": 8500},
{"category": "entertainment", "amount": 25000},
{"category": "transport", "amount": 1500},
{"category": "food", "amount": 15000},
]
# 카테고리별 합계 — 딕셔너리 컴프리헨션
categories = {t["category"] for t in transactions} # 고유 카테고리 셋
totals = {
cat: sum(t["amount"] for t in transactions if t["category"] == cat)
for cat in categories
}
print(totals)
# {'food': 35500, 'transport': 5000, 'entertainment': 25000}
# 금액 > 10000인 거래만
large_tx = [t for t in transactions if t["amount"] >= 10000]
print(f"고액 거래: {len(large_tx)}건")
# 카테고리별 거래 횟수
tx_counts = {cat: sum(1 for t in transactions if t["category"] == cat)
for cat in categories}
print(tx_counts)
# {'food': 3, 'transport': 2, 'entertainment': 1}
가독성 vs 간결성 균형
# 너무 복잡한 컴프리헨션은 오히려 가독성을 해침
# 나쁜 예: 한 줄에 모든 것
result = [f"{k}={v}" for d in [{"a": 1, "b": 2}, {"c": 3}] for k, v in d.items() if v > 1]
# 좋은 예: 변수로 분리
dicts = [{"a": 1, "b": 2}, {"c": 3}]
all_pairs = [(k, v) for d in dicts for k, v in d.items()]
filtered = [f"{k}={v}" for k, v in all_pairs if v > 1]
# 또는 함수로 추출
def process_dict_list(dicts: list[dict]) -> list[str]:
all_items = (item for d in dicts for item in d.items())
return [f"{k}={v}" for k, v in all_items if v > 1]
고수 팁
1. 제너레이터를 함수 인자로 직접 전달
# 괄호가 하나면 제너레이터 표현식의 괄호 생략 가능
total = sum(x**2 for x in range(100)) # OK
joined = ", ".join(str(x) for x in range(5)) # OK
any_neg = any(x < 0 for x in [1, -2, 3]) # OK
print(total, joined, any_neg)
2. walrus 연산자(:=)와 컴프리헨션 조합
# Python 3.8+: 조건 계산 결과 재사용
import re
strings = ["hello123", "world", "python456", "test", "abc789"]
# 변환 결과를 필터 조건에서 재사용
matches = [
m.group()
for s in strings
if (m := re.search(r"\d+", s)) # 매칭 결과를 m에 저장하고 조건에도 사용
]
print(matches) # ['123', '456', '789']
3. 컴프리헨션 내부 변수는 외부로 새지 않음
# 리스트 컴프리헨션: 변수가 외부로 새지 않음 (Python 3)
result = [x for x in range(5)]
# print(x) # NameError: Python 3에서는 x가 스코프 밖으로 새지 않음
# 반면 for 루프의 변수는 루프 후에도 접근 가능
for y in range(5):
pass
print(y) # 4 — 루프 변수는 스코프 밖에서도 접근 가능