리스트 컴프리헨션이란?
리스트 컴프리헨션(List Comprehension)은 기존 리스트를 기반으로 새로운 리스트를 한 줄로 생성하는 파이썬 고유 문법입니다. for 루프를 사용하는 것보다 간결하고, 실행 속도도 빠릅니다.
이 글에서는 기본 문법, 조건부 필터링, 중첩 컴프리헨션, 딕셔너리·셋 컴프리헨션, 그리고 성능 비교까지 다룹니다.
기본 문법
리스트 컴프리헨션의 기본 형태는 [표현식 for 변수 in 반복가능객체]입니다. 아래 예제에서 for 루프 방식과 비교해봅시다.
# for 루프 방식
squares_loop = []
for x in range(10):
squares_loop.append(x ** 2)
# 리스트 컴프리헨션 방식 (동일한 결과)
squares = [x ** 2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
for 루프는 3줄이 필요하지만, 컴프리헨션은 1줄로 동일한 결과를 만들어냅니다. 변수 선언, append 호출이 사라지므로 코드가 훨씬 깔끔합니다.
| 항목 | for 루프 | 컴프리헨션 |
|---|---|---|
| 코드 줄 수 | 3줄 | 1줄 |
| 가독성 | 보통 | 높음 (단순한 경우) |
| 실행 속도 | 느림 | 약 20~30% 빠름 |
| 디버깅 | 쉬움 | 복잡하면 어려움 |
조건부 필터링
if 절을 추가하면 조건을 만족하는 요소만 골라낼 수 있습니다. [표현식 for 변수 in 반복가능객체 if 조건] 형태입니다.
# 짝수만 필터링
numbers = list(range(20))
evens = [x for x in numbers if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# 조건부 변환: if-else는 표현식 위치에 작성
labels = [f"{x}는 짝수" if x % 2 == 0 else f"{x}는 홀수" for x in range(5)]
print(labels) # ['0는 짝수', '1는 홀수', '2는 짝수', '3는 홀수', '4는 짝수']
주의할 점은, **필터링용 if**는 for 뒤에, **삼항 연산자 if-else**는 표현식 자리(for 앞)에 온다는 것입니다. 위치가 다르므로 혼동하지 않도록 합니다.
중첩 리스트 컴프리헨션
2차원 리스트를 1차원으로 펼치거나(flatten), 중첩 반복이 필요할 때 사용합니다. for 절을 여러 개 나열하면 됩니다.
# 2차원 리스트 평탄화
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 구구단 생성 (2단~4단)
gugudan = [f"{i}×{j}={i*j}" for i in range(2, 5) for j in range(1, 10)]
print(gugudan[:5]) # ['2×1=2', '2×2=4', '2×3=6', '2×4=8', '2×5=10']
중첩 컴프리헨션에서 for 절의 순서는 일반 for 루프의 바깥→안쪽 순서와 동일합니다. for row in matrix가 바깥 루프, for num in row가 안쪽 루프입니다.
딕셔너리·셋 컴프리헨션
리스트뿐 아니라 딕셔너리와 셋(set)도 컴프리헨션으로 생성할 수 있습니다. 중괄호 {}를 사용하며, 딕셔너리는 키: 값 형태로 작성합니다.
# 딕셔너리 컴프리헨션: 이름 → 글자 수 매핑
names = ["alice", "bob", "charlie"]
name_lengths = {name: len(name) for name in names}
print(name_lengths) # {'alice': 5, 'bob': 3, 'charlie': 7}
# 셋 컴프리헨션: 중복 제거된 결과
words = ["hello", "world", "hello", "python"]
unique_lengths = {len(w) for w in words}
print(unique_lengths) # {5, 6}
딕셔너리 컴프리헨션은 두 리스트를 zip()으로 묶어 딕셔너리로 변환할 때 특히 유용합니다.
성능 비교
리스트 컴프리헨션은 내부적으로 C 레벨 최적화가 적용되어 일반 for 루프보다 빠릅니다. timeit 모듈로 직접 측정해봅시다.
import timeit
# for 루프 방식
def loop_approach():
result = []
for i in range(1000):
result.append(i ** 2)
return result
# 리스트 컴프리헨션 방식
def comprehension_approach():
return [i ** 2 for i in range(1000)]
loop_time = timeit.timeit(loop_approach, number=10000)
comp_time = timeit.timeit(comprehension_approach, number=10000)
print(f"for 루프: {loop_time:.3f}초") # for 루프: 0.756초
print(f"컴프리헨션: {comp_time:.3f}초") # 컴프리헨션: 0.560초
print(f"속도 향상: {loop_time/comp_time:.1f}배") # 속도 향상: 1.3배
컴프리헨션이 약 1.3배 빠른 이유는, for 루프에서 매번 발생하는 list.append 메서드 조회와 호출 오버헤드가 컴프리헨션에서는 생략되기 때문입니다.
실전 팁과 주의사항
가독성을 최우선으로: 컴프리헨션이 한 줄을 넘기거나 중첩이 3단계 이상이면, 일반 for 루프가 더 낫습니다. 코드는 쓰는 시간보다 읽는 시간이 더 깁니다.
메모리가 중요하면 제너레이터 표현식 사용: 대괄호 [] 대신 소괄호 ()를 쓰면 제너레이터(Generator)가 됩니다. 요소를 한 번에 메모리에 올리지 않고 하나씩 생성하므로, 대량 데이터 처리 시 메모리를 절약할 수 있습니다.
map()/filter()와의 선택: 단순 함수 적용은 map()이, 조건 필터는 filter()가 가독성이 높을 수 있습니다. 그러나 람다가 필요한 경우에는 컴프리헨션이 더 읽기 쉽습니다.
| 상황 | 추천 방식 |
|---|---|
| 단순 변환 | 컴프리헨션 또는 map() |
| 조건 필터링 | 컴프리헨션 |
| 복잡한 로직 (3줄+) | for 루프 |
| 대량 데이터 (메모리 제한) | 제너레이터 표현식 |
정리
리스트 컴프리헨션은 파이썬에서 가장 자주 쓰이는 관용 표현(idiom) 중 하나입니다. 핵심 포인트를 정리하면 다음과 같습니다.
- 기본 형태:
[표현식 for 변수 in 반복가능객체] - 필터링: for 뒤에
if 조건추가 - 삼항 연산자: for 앞에
표현식 if 조건 else 표현식 - 딕셔너리/셋: 중괄호
{}로 동일한 패턴 적용 - 성능: for 루프 대비 약 20~30% 빠름
- 원칙: 한 줄로 읽히지 않으면 for 루프를 사용