리스트·딕셔너리·셋 컴프리헨션과 제너레이터 표현식을 완전히 이해하고 가독성 기준을 익힌다.
🎯 이 강의에서 배우는 것
컴프리헨션(Comprehension)은 파이썬을 파이썬답게 만드는 핵심 문법입니다. 리스트를 만드는 코드가 for 루프보다 빠르고, 딕셔너리 역전이 한 줄로 되며, 10억 개 합계를 메모리 O(1)로 구합니다. 이 강의에서 내부 원리부터 가독성 기준까지 완전히 마스터합니다.
📋 리스트 컴프리헨션 — 탄생 배경과 실행 순서
# 전통적인 방법 (for + append)
squares = []
for x in range(10):
if x % 2 == 0:
squares.append(x ** 2)
print(squares) # [0, 4, 16, 36, 64]
# 리스트 컴프리헨션: [표현식 for 변수 in 이터러블 if 조건]
squares = [x ** 2 for x in range(10) if x % 2 == 0]
print(squares) # [0, 4, 16, 36, 64]
# 실행 순서 암기법: 왼쪽부터 읽으면 거꾸로 됨
# 실제 실행: ① for x in range(10) → ② if x % 2 == 0 → ③ x ** 2
# 즉, for가 먼저, if가 필터, 표현식이 마지막
# 조건이 없을 때
words = ["hello", "world", "python", "code"]
upper = [w.upper() for w in words]
print(upper) # ['HELLO', 'WORLD', 'PYTHON', 'CODE']
# 표현식에 함수 호출
import math
roots = [round(math.sqrt(x), 2) for x in range(1, 6)]
print(roots) # [1.0, 1.41, 1.73, 2.0, 2.24]
⚡ 리스트 컴프리헨션이 for 루프보다 빠른 이유
import timeit
import dis
# 성능 비교
def with_loop():
result = []
for x in range(1000):
result.append(x * 2)
return result
def with_comprehension():
return [x * 2 for x in range(1000)]
loop_time = timeit.timeit(with_loop, number=10000)
comp_time = timeit.timeit(with_comprehension, number=10000)
print(f"for 루프: {loop_time:.3f}초")
print(f"컴프리헨션: {comp_time:.3f}초")
# 컴프리헨션이 약 1.3~1.5배 빠름
# 왜 빠른가? 바이트코드 레벨 차이
# for 루프: result.append(x * 2) → 매번 result 속성 조회 + 함수 호출
# 컴프리헨션: 전용 LIST_APPEND 바이트코드 사용 → 속성 조회 없음
print("=== for 루프 바이트코드 ===")
dis.dis(with_loop)
print("=== 컴프리헨션 바이트코드 ===")
dis.dis(with_comprehension)
# LIST_APPEND 명령어 확인 가능
🔄 중첩 컴프리헨션
# 2차원 리스트 평탄화
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 읽는 순서: for row in matrix → for x in row → x
# 일반 루프와 완전히 같은 순서로 작성하면 됨
# 조건이 있는 중첩
evens_from_matrix = [x for row in matrix for x in row if x % 2 == 0]
print(evens_from_matrix) # [2, 4, 6, 8]
# 2중 컴프리헨션으로 좌표 쌍 생성
pairs = [(x, y) for x in range(3) for y in range(3) if x != y]
print(pairs)
# [(0,1),(0,2),(1,0),(1,2),(2,0),(2,1)]
# 3단계 이상 중첩은 가독성 저하 → 일반 루프 선호
# 컴프리헨션은 2단계까지, 그 이상이면 가독성을 포기하는 것
deep_nested = [x for a in range(3) for b in range(3) for x in range(3)]
# 위보다 아래가 훨씬 읽기 쉬움:
result = []
for a in range(3):
for b in range(3):
for x in range(3):
result.append(x)
📚 딕셔너리·셋 컴프리헨션
# 딕셔너리 컴프리헨션: {키표현식: 값표현식 for 변수 in 이터러블 if 조건}
words = ["apple", "banana", "cherry", "date"]
word_lengths = {word: len(word) for word in words}
print(word_lengths)
# {'apple': 5, 'banana': 6, 'cherry': 6, 'date': 4}
# 키-값 역전 (bijection일 때만 안전)
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}
# 조건부 필터링
scores = {"홍길동": 85, "이순신": 92, "강감찬": 58, "유관순": 78}
passed = {name: score for name, score in scores.items() if score >= 70}
print(passed) # {'홍길동': 85, '이순신': 92, '유관순': 78}
# 값 변환
multiplied = {k: v * 1.1 for k, v in scores.items()}
print(multiplied)
# 셋 컴프리헨션: {표현식 for 변수 in 이터러블 if 조건}
squares_set = {x ** 2 for x in range(-5, 6)}
print(squares_set) # {0, 1, 4, 9, 16, 25} (중복 자동 제거)
print(sorted(squares_set)) # [0, 1, 4, 9, 16, 25]
# 중복 제거 활용
texts = ["apple", "banana", "Apple", "BANANA", "cherry"]
unique_lower = {t.lower() for t in texts}
print(unique_lower) # {'apple', 'banana', 'cherry'}
🌀 제너레이터 표현식 — 메모리 효율의 끝판왕
import sys
# 리스트 컴프리헨션: 모든 값을 메모리에 생성
list_comp = [x ** 2 for x in range(10_000_000)]
print(f"리스트: {sys.getsizeof(list_comp):,} bytes") # ~80MB
# 제너레이터 표현식: () 사용, lazy 평가
gen_exp = (x ** 2 for x in range(10_000_000))
print(f"제너레이터: {sys.getsizeof(gen_exp)} bytes") # ~104 bytes!
# 결과는 같지만 메모리 사용이 극적으로 다름
total_list = sum([x ** 2 for x in range(1_000_000)]) # 리스트 먼저 생성
total_gen = sum(x ** 2 for x in range(1_000_000)) # lazy, O(1) 메모리
print(total_list == total_gen) # True
# 제너레이터는 한 번만 순회 가능!
gen = (x * 2 for x in range(5))
print(list(gen)) # [0, 2, 4, 6, 8]
print(list(gen)) # [] ← 소진됨!
# 함수 인수로 전달할 때 불필요한 괄호 생략 가능
total = sum(x ** 2 for x in range(100)) # (x ** 2 ...) 괄호 생략
maximum = max(len(w) for w in ["hello", "world", "python"])
# 파이프라인: 제너레이터를 연결해 메모리 효율적 처리
def read_large_file(filepath):
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
# lines = read_large_file("huge.txt")
# non_empty = (line for line in lines if line)
# words_count = sum(len(line.split()) for line in non_empty)
# 파일 전체를 메모리에 올리지 않고 단어 수 집계 가능
📏 가독성 기준 — 언제 컴프리헨션, 언제 일반 루프
# ✅ 컴프리헨션이 적합한 경우
# 1. 단순한 변환/필터링
numbers = [1, -2, 3, -4, 5]
positives = [n for n in numbers if n > 0] # ✅ 명확
# 2. 새 컬렉션을 만들 때
squared = {x: x**2 for x in range(1, 6)} # ✅ 직관적
# ❌ 컴프리헨션을 피해야 할 경우
# 1. 부수 효과(side effect)가 있을 때
[print(x) for x in range(5)] # ❌ 출력이 목적이면 for 루프를 쓰세요
# 올바른 방법:
for x in range(5):
print(x)
# 2. 복잡한 조건이나 표현식
# ❌ 읽기 어려움
result = [func1(x) if x > 0 else func2(x) for x in data if predicate(x)]
# ✅ 가독성을 위해 일반 루프
result = []
for x in data:
if predicate(x):
result.append(func1(x) if x > 0 else func2(x))
# 3. 결과를 쓰지 않을 때
# ❌ 리스트를 만들지만 버림 (메모리 낭비)
[process(item) for item in data] # 쓰지 않는 리스트 생성
# ✅ 올바른 방법
for item in data:
process(item)
⚠️ 자주 하는 실수
- 실행 순서 혼동:
[표현식 for x in ... if 조건]에서 for가 먼저, if가 필터, 표현식이 마지막입니다. 표현식 안에서 x가 안 보이는 경우는 for/if 순서가 틀린 것입니다. - 제너레이터를 두 번 순회:
(x for x in ...)는 한 번 소진됩니다. 여러 번 써야 하면list()로 변환하세요. - 컴프리헨션에 부수 효과 넣기: 출력·DB 쓰기·상태 변경을 컴프리헨션에 넣으면 "값을 만들기 위한 문법"이 아닌 "효과를 위한 문법"이 되어 코드의 의도를 흐립니다.
📝 정리 및 다음 강의 예고
- 리스트 컴프리헨션:
[표현식 for x in 이터러블 if 조건]— for가 먼저, if가 필터, 표현식이 마지막. - 딕셔너리 컴프리헨션
{k:v ...}와 셋 컴프리헨션{x ...}도 같은 원리. - 제너레이터 표현식
(표현식 for ...)은 lazy — 메모리 O(1)으로 거대한 데이터를 처리합니다. - 단순 변환/필터는 컴프리헨션, 복잡하거나 부수 효과가 있으면 일반 루프.
다음 강의: 함수란 무엇인가 — 추상화의 본질, 일급 객체로서의 함수, 그리고 올바른 docstring 작성법을 배웁니다.
관련 주제
- 리스트 컴프리헨션
- 딕셔너리 컴프리헨션
- 셋 컴프리헨션
- 제너레이터 표현식
- 중첩 컴프리헨션 평탄화
- LIST_APPEND 바이트코드
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
파이썬 기초 40강 — 처음 배우는 프로그래밍
이 강의가 속한 시리즈는 총 32강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
0/1000
불러오는 중...
