Python 컴프리헨션 | 리스트, 딕셔너리, 세트 컴프리헨션 완벽 정리
이 글의 핵심
Python 컴프리헨션에 대한 실전 가이드입니다. 리스트, 딕셔너리, 세트 컴프리헨션 완벽 정리 등을 예제와 함께 상세히 설명합니다.
들어가며
”한 줄로 리스트 만들기”
컴프리헨션은 간결하고 빠른 Python의 강력한 기능입니다.
1. 리스트 컴프리헨션 (List Comprehension)
리스트 컴프리헨션이란?
리스트 컴프리헨션은 한 줄로 리스트를 생성하는 Python의 강력한 기능입니다. 일반 for 루프보다 간결하고 빠릅니다.
문법: [표현식 for 변수 in 반복가능객체 if 조건]
기본 문법과 동작 원리
아래 왼쪽은 빈 장바구니에 for 루프로 하나씩 담는 방식이고, 오른쪽 한 줄은 같은 일을 컴프리헨션으로 압축한 것입니다. 읽을 때는 for i in range(10)을 먼저 읽고, 앞쪽 i ** 2가 각 반복에서 리스트에 들어간다고 보면 됩니다.
# 일반 for 루프 (전통적 방식)
squares = []
for i in range(10):
squares.append(i ** 2)
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# 리스트 컴프리헨션 (Python 스타일)
squares = [i ** 2 for i in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# 동작 과정:
# 1. range(10)에서 i를 하나씩 가져옴 (0, 1, 2, ..., 9)
# 2. 각 i에 대해 i ** 2 계산
# 3. 결과를 리스트에 추가
# 4. 최종 리스트 반환
성능 비교:
import time
# 방법 1: for 루프
start = time.time()
result1 = []
for i in range(1000000):
result1.append(i ** 2)
print(f"for 루프: {time.time() - start:.4f}초") # 약 0.15초
# 방법 2: 리스트 컴프리헨션
start = time.time()
result2 = [i ** 2 for i in range(1000000)]
print(f"컴프리헨션: {time.time() - start:.4f}초") # 약 0.10초
# 컴프리헨션이 약 30% 빠름!
조건문 추가 (필터링)
if 조건으로 특정 요소만 선택할 수 있습니다.
# 짝수만 선택
evens = [i for i in range(10) if i % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
# 동작 과정:
# i=0: 0 % 2 == 0 → True → 0 추가
# i=1: 1 % 2 == 0 → False → 무시
# i=2: 2 % 2 == 0 → True → 2 추가
# ...
# 3의 배수이면서 10보다 큰 수
multiples = [i for i in range(30) if i % 3 == 0 and i > 10]
print(multiples) # [12, 15, 18, 21, 24, 27]
# 문자열 필터링
words = ['apple', 'banana', 'cherry', 'date', 'elderberry']
long_words = [word for word in words if len(word) > 5]
print(long_words) # ['banana', 'cherry', 'elderberry']
if-else 표현식 (변환)
if-else를 사용하여 요소를 변환할 수 있습니다.
# 짝수/홀수 라벨링
labels = ['짝수' if i % 2 == 0 else '홀수' for i in range(5)]
print(labels) # ['짝수', '홀수', '짝수', '홀수', '짝수']
# 문법 주의: if-else는 for 앞에
# [표현식 if 조건 else 다른표현식 for 변수 in 반복가능객체]
# 양수/음수/0 분류
numbers = [-2, -1, 0, 1, 2]
signs = [
'양수' if n > 0 else ('음수' if n < 0 else '0')
for n in numbers
]
print(signs) # ['음수', '음수', '0', '양수', '양수']
# 점수를 등급으로 변환
scores = [95, 85, 75, 65, 55]
grades = [
'A' if s >= 90 else 'B' if s >= 80 else 'C' if s >= 70 else 'D' if s >= 60 else 'F'
for s in scores
]
print(grades) # ['A', 'B', 'C', 'D', 'F']
if vs if-else 위치 차이:
# 필터링 (if만): for 뒤에
evens = [i for i in range(10) if i % 2 == 0]
# 변환 (if-else): for 앞에
labels = ['짝수' if i % 2 == 0 else '홀수' for i in range(10)]
# 필터링 + 변환: 둘 다 사용
# [변환표현식 for 변수 in 반복 if 필터조건]
positive_squares = [i ** 2 if i > 0 else 0 for i in range(-5, 6) if i != 0]
print(positive_squares)
중첩 반복문 (Nested Loops)
중첩 for 루프를 한 줄로 표현할 수 있습니다.
# 2차원 리스트 평탄화 (Flatten)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# 일반 for 루프
flat = []
for row in matrix:
for num in row:
flat.append(num)
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 리스트 컴프리헨션 (왼쪽 for가 외부, 오른쪽 for가 내부)
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 읽는 순서:
# for row in matrix: # 외부 루프
# for num in row: # 내부 루프
# num # 표현식
# 구구단 생성
multiplication_table = [
f"{i} x {j} = {i*j}"
for i in range(2, 10) # 2단부터 9단
for j in range(1, 10) # 1부터 9까지
]
print(multiplication_table[:5])
# ['2 x 1 = 2', '2 x 2 = 4', '2 x 3 = 6', '2 x 4 = 8', '2 x 5 = 10']
# 좌표 생성
coordinates = [(x, y) for x in range(3) for y in range(3)]
print(coordinates)
# [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
# 조건 추가: 대각선만
diagonal = [(x, y) for x in range(5) for y in range(5) if x == y]
print(diagonal) # [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)]
중첩 컴프리헨션 vs 중첩 리스트:
# 2차원 리스트 생성 (행렬)
# 방법 1: 중첩 컴프리헨션
matrix = [[i * j for j in range(5)] for i in range(5)]
print(matrix)
# [[0, 0, 0, 0, 0],
# [0, 1, 2, 3, 4],
# [0, 2, 4, 6, 8],
# [0, 3, 6, 9, 12],
# [0, 4, 8, 12, 16]]
# 방법 2: 일반 for 루프
matrix = []
for i in range(5):
row = []
for j in range(5):
row.append(i * j)
matrix.append(row)
2. 딕셔너리 컴프리헨션 (Dictionary Comprehension)
딕셔너리 컴프리헨션이란?
딕셔너리 컴프리헨션은 키-값 쌍을 한 줄로 생성하는 기능입니다.
문법: {키표현식: 값표현식 for 변수 in 반복가능객체 if 조건}
기본 문법과 동작 원리
# 일반 for 루프
squares_dict = {}
for i in range(5):
squares_dict[i] = i ** 2
print(squares_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# 딕셔너리 컴프리헨션
squares_dict = {i: i ** 2 for i in range(5)}
print(squares_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# 동작 과정:
# i=0: 키=0, 값=0**2=0 → {0: 0}
# i=1: 키=1, 값=1**2=1 → {0: 0, 1: 1}
# i=2: 키=2, 값=2**2=4 → {0: 0, 1: 1, 2: 4}
# ...
# 리스트를 딕셔너리로 변환
names = ['철수', '영희', '민수']
name_dict = {i: name for i, name in enumerate(names)}
print(name_dict) # {0: '철수', 1: '영희', 2: '민수'}
# enumerate() 설명:
# enumerate(names) = [(0, '철수'), (1, '영희'), (2, '민수')]
# 각 (i, name) 튜플에서 i는 키, name은 값
조건부 딕셔너리 (필터링)
# 짝수만 딕셔너리에 추가
even_squares = {i: i ** 2 for i in range(10) if i % 2 == 0}
print(even_squares) # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
# 특정 값만 필터링
scores = {'철수': 85, '영희': 92, '민수': 78, '지영': 95}
high_scores = {name: score for name, score in scores.items() if score >= 90}
print(high_scores) # {'영희': 92, '지영': 95}
# 키와 값 모두 조건 적용
data = {'apple': 5, 'banana': 3, 'cherry': 8, 'date': 2}
filtered = {
k.upper(): v * 2
for k, v in data.items()
if len(k) > 4 and v > 3
}
print(filtered) # {'APPLE': 10, 'CHERRY': 16}
키-값 변환 (Swap)
# 키와 값 교환
original = {'a': 1, 'b': 2, 'c': 3}
swapped = {v: k for k, v in original.items()}
print(swapped) # {1: 'a', 2: 'b', 3: 'c'}
# 주의: 값이 중복되면 마지막 값만 남음
original = {'a': 1, 'b': 2, 'c': 1}
swapped = {v: k for k, v in original.items()}
print(swapped) # {1: 'c', 2: 'b'} ('a'는 'c'에 덮어씌워짐)
# 해결: 값을 리스트로 수집
from collections import defaultdict
swapped_multi = defaultdict(list)
for k, v in original.items():
swapped_multi[v].append(k)
print(dict(swapped_multi)) # {1: ['a', 'c'], 2: ['b']}
실전 예제: 데이터 변환
# 예제 1: 문자열 길이 매핑
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}
# 예제 2: 환경 변수 파싱
env_str = "DEBUG=True,PORT=8000,HOST=localhost"
env_dict = {
pair.split('=')[0]: pair.split('=')[1]
for pair in env_str.split(',')
}
print(env_dict)
# {'DEBUG': 'True', 'PORT': '8000', 'HOST': 'localhost'}
# 예제 3: 두 리스트를 딕셔너리로 결합
keys = ['name', 'age', 'city']
values = ['Alice', 25, 'Seoul']
person = {k: v for k, v in zip(keys, values)}
print(person) # {'name': 'Alice', 'age': 25, 'city': 'Seoul'}
# 예제 4: 딕셔너리 필터링 및 변환
products = {
'apple': 1000,
'banana': 500,
'cherry': 2000,
'date': 800
}
# 1000원 이상 제품에 10% 할인
discounted = {
name: price * 0.9
for name, price in products.items()
if price >= 1000
}
print(discounted) # {'apple': 900.0, 'cherry': 1800.0}
3. 세트 컴프리헨션 (Set Comprehension)
세트 컴프리헨션이란?
세트 컴프리헨션은 중복 없는 집합을 한 줄로 생성합니다. 리스트 컴프리헨션과 비슷하지만 [] 대신 {}를 사용합니다.
문법: {표현식 for 변수 in 반복가능객체 if 조건}
기본 문법과 중복 제거
# 중복이 있는 리스트
numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
# 일반 방법: set() 사용
unique = set(numbers)
print(unique) # {1, 2, 3, 4}
# 세트 컴프리헨션
unique = {n for n in numbers}
print(unique) # {1, 2, 3, 4}
# 변환 + 중복 제거
numbers = [1, -2, 2, -3, 3, -4, 4]
abs_unique = {abs(n) for n in numbers}
print(abs_unique) # {1, 2, 3, 4} (음수가 양수로 변환되며 중복 제거)
조건부 세트
# 짝수만
even_set = {i for i in range(10) if i % 2 == 0}
print(even_set) # {0, 2, 4, 6, 8}
# 문자열에서 모음만 추출
text = "Hello World"
vowels = {char.lower() for char in text if char.lower() in 'aeiou'}
print(vowels) # {'e', 'o'} (중복 'o' 자동 제거)
# 리스트에서 길이 3 이상 단어만
words = ['hi', 'hello', 'hey', 'hello', 'world', 'hi']
long_words = {word for word in words if len(word) >= 3}
print(long_words) # {'hello', 'hey', 'world'} (중복 제거됨)
실전 예제
# 예제 1: 이메일 도메인 추출
emails = [
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]'
]
domains = {email.split('@')[1] for email in emails}
print(domains) # {'gmail.com', 'yahoo.com', 'outlook.com'}
# 예제 2: 파일 확장자 수집
files = ['image.jpg', 'doc.pdf', 'photo.jpg', 'video.mp4', 'report.pdf']
extensions = {file.split('.')[-1] for file in files}
print(extensions) # {'jpg', 'pdf', 'mp4'}
# 예제 3: 숫자의 마지막 자리 수
numbers = [123, 456, 789, 111, 222, 333]
last_digits = {n % 10 for n in numbers}
print(last_digits) # {1, 2, 3, 6, 9}
세트 연산과 결합
# 두 리스트의 공통 요소
list1 = [1, 2, 3, 4, 5]
list2 = [4, 5, 6, 7, 8]
common = {x for x in list1} & {x for x in list2}
print(common) # {4, 5}
# 또는 더 간단하게
common = set(list1) & set(list2)
print(common) # {4, 5}
# 차집합: list1에만 있는 요소
diff = {x for x in list1} - {x for x in list2}
print(diff) # {1, 2, 3}
# 합집합
union = {x for x in list1} | {x for x in list2}
print(union) # {1, 2, 3, 4, 5, 6, 7, 8}
4. 제너레이터 표현식 (Generator Expression)
제너레이터 표현식이란?
제너레이터 표현식은 필요할 때만 값을 생성하는 메모리 효율적인 방법입니다. 리스트 컴프리헨션과 문법이 같지만 [] 대신 ()를 사용합니다.
문법: (표현식 for 변수 in 반복가능객체 if 조건)
기본 문법과 동작 원리
# 리스트 컴프리헨션: 모든 값을 메모리에 저장
squares_list = [i ** 2 for i in range(1000000)]
print(type(squares_list)) # <class 'list'>
print(len(squares_list)) # 1000000 (모든 요소가 메모리에 존재)
# 제너레이터 표현식: 필요할 때만 생성
squares_gen = (i ** 2 for i in range(1000000))
print(type(squares_gen)) # <class 'generator'>
# print(len(squares_gen)) # TypeError: object of type 'generator' has no len()
# next()로 하나씩 가져오기
print(next(squares_gen)) # 0 (첫 번째 값 생성)
print(next(squares_gen)) # 1 (두 번째 값 생성)
print(next(squares_gen)) # 4 (세 번째 값 생성)
# for 루프에서 사용 (자동으로 next() 호출)
for square in (i ** 2 for i in range(5)):
print(square, end=' ') # 0 1 4 9 16
print()
# 제너레이터는 한 번만 순회 가능
gen = (i for i in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [] (이미 소진됨)
메모리 효율성 비교
import sys
# 리스트: 모든 요소를 메모리에 저장
list_comp = [i for i in range(100000)]
print(f"리스트 크기: {sys.getsizeof(list_comp):,} bytes") # ~800,000 bytes
# 제너레이터: 객체만 저장
gen_expr = (i for i in range(100000))
print(f"제너레이터 크기: {sys.getsizeof(gen_expr):,} bytes") # ~120 bytes
# 차이: 약 6,600배!
언제 제너레이터를 사용할까?
# 1. sum, max, min 등 집계 함수와 함께
total = sum(i ** 2 for i in range(1000000)) # 메모리 효율적
maximum = max(i ** 2 for i in range(1000))
# 2. any, all 함수 (조기 종료 가능)
# 10000보다 큰 제곱수가 있는지 확인
has_large = any(i ** 2 > 10000 for i in range(1000000))
print(has_large) # True (i=101에서 조기 종료)
# 3. 파일 처리
with open('large_file.txt') as f:
# 빈 줄이 아닌 줄 수 계산
non_empty_lines = sum(1 for line in f if line.strip())
# 4. 체이닝
# 제너레이터를 여러 단계로 체이닝
numbers = range(1000000)
evens = (x for x in numbers if x % 2 == 0)
squares = (x ** 2 for x in evens)
large = (x for x in squares if x > 100)
result = sum(large) # 메모리 효율적으로 처리
제너레이터 vs 리스트 선택 가이드
# ✅ 제너레이터 사용
# - 한 번만 순회
# - 메모리가 제한적
# - 무한 시퀀스
total = sum(i ** 2 for i in range(1000000))
# ✅ 리스트 사용
# - 여러 번 순회
# - 인덱싱 필요
# - 길이 확인 필요
squares = [i ** 2 for i in range(10)]
print(squares[5]) # 인덱싱
print(len(squares)) # 길이
print(sum(squares)) # 첫 번째 순회
print(max(squares)) # 두 번째 순회
5. 실전 예제
예제 1: CSV 데이터 파싱
CSV 문자열을 딕셔너리 리스트로 변환하는 실용적인 예제입니다.
# CSV 데이터 (헤더 + 데이터 행)
csv_data = "name,age,city\n철수,25,서울\n영희,30,부산\n민수,28,대전"
# 1단계: 줄 단위로 분리
lines = csv_data.strip().split('\n')
print(lines)
# ['name,age,city', '철수,25,서울', '영희,30,부산', '민수,28,대전']
# 2단계: 헤더 추출
header = lines[0].split(',')
print(header) # ['name', 'age', 'city']
# 3단계: 데이터 행을 딕셔너리로 변환
data = [
dict(zip(header, line.split(',')))
for line in lines[1:]
]
print(data)
# [{'name': '철수', 'age': '25', 'city': '서울'},
# {'name': '영희', 'age': '30', 'city': '부산'},
# {'name': '민수', 'age': '28', 'city': '대전'}]
# zip() 설명:
# header = ['name', 'age', 'city']
# line.split(',') = ['철수', '25', '서울']
# zip(header, line.split(',')) = [('name', '철수'), ('age', '25'), ('city', '서울')]
# dict(...) = {'name': '철수', 'age': '25', 'city': '서울'}
타입 변환 추가:
# age를 정수로 변환
data_typed = [
{
'name': parts[0],
'age': int(parts[1]), # 문자열 → 정수
'city': parts[2]
}
for line in lines[1:]
for parts in [line.split(',')] # 변수 재사용 트릭
]
print(data_typed)
# [{'name': '철수', 'age': 25, 'city': '서울'}, ...]
예제 2: 학생 성적 처리
필터링과 변환을 동시에 수행하는 예제입니다.
# 학생 데이터
students = [
{'name': '철수', 'score': 85},
{'name': '영희', 'score': 92},
{'name': '민수', 'score': 78},
{'name': '지영', 'score': 95},
{'name': '수진', 'score': 88}
]
# 필터링: 90점 이상 학생 이름만
high_scores = [
s['name'] for s in students if s['score'] >= 90
]
print(high_scores) # ['영희', '지영']
# 변환: 등급 추가
graded = [
{**s, 'grade': 'A' if s['score'] >= 90 else 'B' if s['score'] >= 80 else 'C'}
for s in students
]
print(graded)
# [{'name': '철수', 'score': 85, 'grade': 'B'},
# {'name': '영희', 'score': 92, 'grade': 'A'},
# {'name': '민수', 'score': 78, 'grade': 'C'},
# {'name': '지영', 'score': 95, 'grade': 'A'},
# {'name': '수진', 'score': 88, 'grade': 'B'}]
# {**s, 'grade': ...} 설명:
# **s: 딕셔너리 언패킹 (기존 키-값 복사)
# 'grade': ...: 새 키 추가
# 결과: 기존 딕셔너리 + 새 키
# 필터링 + 변환: 80점 이상만 등급 부여
passed = [
{**s, 'grade': 'A' if s['score'] >= 90 else 'B'}
for s in students
if s['score'] >= 80
]
print(passed)
# [{'name': '철수', 'score': 85, 'grade': 'B'},
# {'name': '영희', 'score': 92, 'grade': 'A'},
# {'name': '지영', 'score': 95, 'grade': 'A'},
# {'name': '수진', 'score': 88, 'grade': 'B'}]
예제 3: 문자열 처리
# 문자열 리스트 정리
names = [' alice ', 'BOB', ' Charlie', 'david ']
# 공백 제거 + 소문자 변환
cleaned = [name.strip().lower() for name in names]
print(cleaned) # ['alice', 'bob', 'charlie', 'david']
# 첫 글자 대문자
capitalized = [name.strip().capitalize() for name in names]
print(capitalized) # ['Alice', 'Bob', 'Charlie', 'David']
# 길이 3 이상인 이름만
filtered = [name.strip() for name in names if len(name.strip()) >= 3]
print(filtered) # ['alice', 'BOB', 'Charlie', 'david']
예제 4: 파일 경로 처리
import os
# 파일 목록에서 .txt 파일만 추출
files = ['data.txt', 'image.png', 'report.txt', 'video.mp4', 'notes.txt']
txt_files = [f for f in files if f.endswith('.txt')]
print(txt_files) # ['data.txt', 'report.txt', 'notes.txt']
# 확장자 제거
names_only = [os.path.splitext(f)[0] for f in txt_files]
print(names_only) # ['data', 'report', 'notes']
# 전체 경로 생성
base_path = '/home/user/documents'
full_paths = [os.path.join(base_path, f) for f in txt_files]
print(full_paths)
# ['/home/user/documents/data.txt',
# '/home/user/documents/report.txt',
# '/home/user/documents/notes.txt']
예제 5: API 응답 처리
# API 응답 (JSON 형태)
api_response = {
'users': [
{'id': 1, 'name': 'Alice', 'active': True, 'age': 25},
{'id': 2, 'name': 'Bob', 'active': False, 'age': 30},
{'id': 3, 'name': 'Charlie', 'active': True, 'age': 35},
{'id': 4, 'name': 'David', 'active': True, 'age': 28}
]
}
# 활성 사용자 ID만 추출
active_ids = [
user['id']
for user in api_response['users']
if user['active']
]
print(active_ids) # [1, 3, 4]
# 활성 사용자 이름과 나이
active_users = [
{'name': user['name'], 'age': user['age']}
for user in api_response['users']
if user['active']
]
print(active_users)
# [{'name': 'Alice', 'age': 25},
# {'name': 'Charlie', 'age': 35},
# {'name': 'David', 'age': 28}]
# 30세 이상 활성 사용자
senior_active = [
user['name']
for user in api_response['users']
if user['active'] and user['age'] >= 30
]
print(senior_active) # ['Charlie']
6. 성능 고려사항
메모리 효율성: 리스트 vs 제너레이터
리스트 컴프리헨션은 모든 요소를 메모리에 저장하지만, 제너레이터 표현식은 필요할 때만 생성합니다.
import sys
# 리스트 컴프리헨션: 전체 리스트를 메모리에 생성
squares_list = [i ** 2 for i in range(1000000)]
# 제너레이터 표현식: 필요할 때만 생성 (괄호 사용)
squares_gen = (i ** 2 for i in range(1000000))
# 메모리 사용량 비교
print(f"리스트: {sys.getsizeof(squares_list):,} bytes") # ~8,000,000 bytes (8MB)
print(f"제너레이터: {sys.getsizeof(squares_gen):,} bytes") # ~120 bytes
# 제너레이터는 한 번에 하나씩만 생성
for i, square in enumerate(squares_gen):
if i >= 5:
break
print(square) # 0, 1, 4, 9, 16
언제 제너레이터를 사용할까?
# 1. 큰 데이터를 한 번만 순회할 때
total = sum(i ** 2 for i in range(1000000)) # 제너레이터 사용
# 2. 파일 처리
with open('large_file.txt') as f:
# 각 줄의 단어 수 계산 (메모리 효율적)
word_counts = (len(line.split()) for line in f)
total_words = sum(word_counts)
# 3. 무한 시퀀스
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 처음 10개만
first_10 = [fib for i, fib in enumerate(fibonacci()) if i < 10]
print(first_10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
리스트가 필요한 경우:
# 1. 여러 번 순회해야 할 때
numbers = [i for i in range(10)]
print(sum(numbers)) # 첫 번째 순회
print(max(numbers)) # 두 번째 순회 (제너레이터는 소진되어 불가)
# 2. 인덱싱이 필요할 때
squares = [i ** 2 for i in range(10)]
print(squares[5]) # 25 (제너레이터는 인덱싱 불가)
# 3. 길이를 알아야 할 때
print(len(squares)) # 10 (제너레이터는 len() 불가)
성능 벤치마크
import time
# 테스트 데이터
data = list(range(1000000))
# 1. 리스트 컴프리헨션
start = time.time()
result1 = [x * 2 for x in data if x % 2 == 0]
time1 = time.time() - start
print(f"컴프리헨션: {time1:.4f}초") # 약 0.08초
# 2. 일반 for 루프 + append
start = time.time()
result2 = []
for x in data:
if x % 2 == 0:
result2.append(x * 2)
time2 = time.time() - start
print(f"for 루프: {time2:.4f}초") # 약 0.12초
# 3. map + filter
start = time.time()
result3 = list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, data)))
time3 = time.time() - start
print(f"map+filter: {time3:.4f}초") # 약 0.15초
# 결과: 컴프리헨션이 가장 빠르고 가독성도 좋음!
중첩 컴프리헨션 주의사항
# ❌ 너무 복잡: 3중 중첩
result = [
z
for x in range(10)
for y in range(10)
for z in range(10)
if x < y < z
]
# ✅ 개선: 함수로 분리
def is_ascending(x, y, z):
return x < y < z
result = [
z
for x in range(10)
for y in range(10)
for z in range(10)
if is_ascending(x, y, z)
]
# ✅ 더 나은 방법: itertools 사용
from itertools import combinations
result = [c[2] for c in combinations(range(10), 3)]
7. 컴프리헨션과 for 루프, 언제 무엇을 쓸까
리스트 컴프리헨션은 장바구니에 담을 물건을 한 줄로 고르는 진열처럼 읽기 쉽습니다. 분기가 많아지거나 부작용(파일 쓰기 등)이 섞이면 일반 for 루프가 의도를 드러내기 쉽습니다. 한 줄에 조건이 너무 많이 붙으면 디버깅이 어려워지므로, 그때는 루프로 쪼개는 편이 낫습니다.
# ✅ 컴프리헨션 사용 (간단한 변환)
squares = [x ** 2 for x in range(10)]
# ✅ 반복문 사용 (복잡한 로직)
result = []
for x in range(10):
if x % 2 == 0:
temp = x ** 2
if temp > 20:
result.append(temp)
else:
result.append(temp * 2)
# ❌ 너무 복잡한 컴프리헨션 (가독성 나쁨)
result = [
x ** 2 if x % 2 == 0 and x ** 2 > 20 else x ** 2 * 2
for x in range(10) if x % 2 == 0
]
일반적인 실수와 해결법
실수 1: 리스트 복사 문제
# ❌ 잘못된 방법: 같은 리스트를 여러 번 참조
matrix = [[0] * 3] * 3
matrix[0][0] = 1
print(matrix) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] (모든 행이 변경됨!)
# ✅ 올바른 방법: 리스트 컴프리헨션으로 각 행을 독립적으로 생성
matrix = [[0] * 3 for _ in range(3)]
matrix[0][0] = 1
print(matrix) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]] (첫 행만 변경)
실수 2: 불필요한 리스트 생성
# ❌ 비효율: 리스트를 만들고 다시 합산
total = sum([i ** 2 for i in range(1000000)]) # 8MB 메모리 사용
# ✅ 효율적: 제너레이터로 바로 합산
total = sum(i ** 2 for i in range(1000000)) # 120 bytes만 사용
실수 3: 부작용(Side Effect) 있는 함수 사용
# ❌ 나쁜 예: 컴프리헨션에서 부작용 발생
results = []
[results.append(x * 2) for x in range(10)] # append의 반환값(None)으로 리스트 생성
# ✅ 좋은 예: 일반 for 루프 사용
results = []
for x in range(10):
results.append(x * 2)
# ✅ 또는 컴프리헨션으로 직접 생성
results = [x * 2 for x in range(10)]
디버깅 팁
# 복잡한 컴프리헨션 디버깅
data = [1, 2, 3, 4, 5]
# 한 줄로 작성 (디버깅 어려움)
result = [x ** 2 for x in data if x % 2 == 0]
# 단계별로 분리 (디버깅 쉬움)
filtered = [x for x in data if x % 2 == 0]
print(f"필터링 결과: {filtered}") # [2, 4]
result = [x ** 2 for x in filtered]
print(f"최종 결과: {result}") # [4, 16]
실전 활용 패턴
패턴 1: 데이터 정규화
# 이름 정규화
raw_names = [' ALICE ', 'bob', ' Charlie ', 'DAVID']
normalized = [name.strip().title() for name in raw_names]
print(normalized) # ['Alice', 'Bob', 'Charlie', 'David']
패턴 2: 조건부 집계
# 짝수의 합, 홀수의 합
numbers = range(1, 11)
even_sum = sum(x for x in numbers if x % 2 == 0)
odd_sum = sum(x for x in numbers if x % 2 == 1)
print(f"짝수 합: {even_sum}, 홀수 합: {odd_sum}") # 짝수 합: 30, 홀수 합: 25
패턴 3: 데이터 그룹화
# 점수별 학생 그룹화
students = [
{'name': '철수', 'score': 85},
{'name': '영희', 'score': 92},
{'name': '민수', 'score': 85},
{'name': '지영', 'score': 92}
]
# 점수별로 이름 그룹화
from collections import defaultdict
grouped = defaultdict(list)
[grouped[s['score']].append(s['name']) for s in students]
print(dict(grouped)) # {85: ['철수', '민수'], 92: ['영희', '지영']}
# 더 나은 방법: itertools.groupby 사용 (권장)
from itertools import groupby
students_sorted = sorted(students, key=lambda s: s['score'])
grouped_better = {
score: [s['name'] for s in group]
for score, group in groupby(students_sorted, key=lambda s: s['score'])
}
print(grouped_better) # {85: ['철수', '민수'], 92: ['영희', '지영']}
8. 트러블슈팅
문제 1: “list index out of range”
# 문제 상황
data = [[1, 2], [3, 4, 5], [6]]
# result = [row[2] for row in data] # IndexError!
# 해결 1: 조건 추가
result = [row[2] for row in data if len(row) > 2]
print(result) # [5]
# 해결 2: 기본값 사용
result = [row[2] if len(row) > 2 else None for row in data]
print(result) # [None, 5, None]
문제 2: 딕셔너리 키 중복
# 문제: 마지막 값만 남음
items = [('a', 1), ('b', 2), ('a', 3)]
d = {k: v for k, v in items}
print(d) # {'a': 3, 'b': 2} (첫 번째 'a': 1이 덮어씌워짐)
# 해결: 값을 리스트로 수집
from collections import defaultdict
d = defaultdict(list)
[d[k].append(v) for k, v in items]
print(dict(d)) # {'a': [1, 3], 'b': [2]}
문제 3: 컴프리헨션 내부에서 예외 처리
# 문제: 일부 데이터가 잘못된 형식
data = ['1', '2', 'three', '4', 'five']
# ❌ 에러 발생
# result = [int(x) for x in data] # ValueError: invalid literal for int()
# ✅ 해결 1: 조건으로 필터링
result = [int(x) for x in data if x.isdigit()]
print(result) # [1, 2, 4]
# ✅ 해결 2: 함수로 예외 처리
def safe_int(x):
try:
return int(x)
except ValueError:
return None
result = [safe_int(x) for x in data]
print(result) # [1, 2, None, 4, None]
# None 제거
result_filtered = [x for x in result if x is not None]
print(result_filtered) # [1, 2, 4]
9. 상황별 요약과 코드 리뷰 체크리스트
아래 표는 7절에서 말한 “간단하면 컴프리헨션, 복잡하면 for”를 상황별로 정리한 것입니다. 예제 코드는 7절과 동일하므로, 필요하면 7절의 설명과 함께 참고하시면 됩니다.
| 상황 | 추천 방법 | 이유 |
|---|---|---|
| 간단한 변환/필터링 | 컴프리헨션 | 간결하고 빠름 |
| 복잡한 조건 로직 | 일반 for 루프 | 가독성, 디버깅 |
| 부작용(파일 쓰기, DB 저장) | 일반 for 루프 | 명확한 의도 표현 |
| 대용량 데이터 한 번 순회 | 제너레이터 | 메모리 효율 |
| 여러 번 순회 필요 | 리스트 컴프리헨션 | 재사용 가능 |
코드 리뷰 체크리스트
- 한 줄이 80자를 넘지 않는가? (PEP 8)
- 중첩이 2단계를 넘지 않는가?
- 다른 개발자가 5초 안에 이해할 수 있는가?
- 제너레이터로 대체 가능한가? (메모리 최적화)
- 예외 처리가 필요한가?
10. 연습 문제
문제 1: 기본 변환
# 1부터 20까지 중 3의 배수의 제곱을 리스트로 만드세요
# 정답: [9, 36, 81, 144, 225, 324]
문제 2: 딕셔너리 변환
# 다음 리스트를 {이름: 나이} 딕셔너리로 변환하세요
people = [('Alice', 25), ('Bob', 30), ('Charlie', 35)]
# 정답: {'Alice': 25, 'Bob': 30, 'Charlie': 35}
문제 3: 중첩 리스트 처리
# 2차원 리스트에서 짝수만 추출하여 평탄화하세요
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# 정답: [2, 4, 6, 8]
문제 4: 조건부 변환
# 온도 리스트를 "추움"(10 미만), "적당"(10-25), "더움"(25 초과)으로 분류하세요
temps = [5, 15, 30, 8, 22, 28]
# 정답: ['추움', '적당', '더움', '추움', '적당', '더움']
정답 보기
# 문제 1
multiples_of_3 = [x ** 2 for x in range(1, 21) if x % 3 == 0]
print(multiples_of_3)
# 문제 2
people_dict = {name: age for name, age in people}
print(people_dict)
# 문제 3
evens_flat = [num for row in matrix for num in row if num % 2 == 0]
print(evens_flat)
# 문제 4
labels = [
'추움' if t < 10 else '적당' if t <= 25 else '더움'
for t in temps
]
print(labels)
정리
핵심 요약
- 리스트 컴프리헨션:
[표현식 for 변수 in 반복가능객체 if 조건]- 간결하고 빠른 리스트 생성 - 딕셔너리 컴프리헨션:
{키: 값 for ...}- 키-값 쌍 변환 - 세트 컴프리헨션:
{표현식 for ...}- 중복 제거 자동 - 제너레이터 표현식:
(표현식 for ...)- 메모리 효율적 - 가독성 우선: 복잡하면 일반 반복문 사용
컴프리헨션을 마스터하면
- 코드가 30% 더 빠르고 간결해집니다
- Python 스타일의 관용적 코드(Pythonic)를 작성할 수 있습니다
- 데이터 처리 작업이 훨씬 쉬워집니다
다음 단계
- 데코레이터 - 함수를 꾸미는 고급 기법
- 제너레이터 함수 - yield와 무한 시퀀스
- 람다와 고차 함수 - 함수형 프로그래밍
관련 글
- Python 환경 설정 | Windows/Mac에서 Python 설치하고 시작하기