Python 컴프리헨션 | 리스트, 딕셔너리, 세트 컴프리헨션 완벽 정리
이 글의 핵심
Python 컴프리헨션: 리스트, 딕셔너리, 세트 컴프리헨션 리스트 컴프리헨션 (List Comprehension)부터 핵심 개념·패턴·실무 함정을 코드 예제로 풉니다.
들어가며
”한 줄로 리스트 만들기”
컴프리헨션은 간결하고 빠른 Python의 강력한 기능입니다.
실무 활용 사례: 데이터 분석, 웹 개발, 자동화 프로젝트에서 실제로 사용한 패턴과 코드를 바탕으로 정리했습니다. 초보자가 흔히 겪는 오류와 해결법을 포함합니다.
실무에서 느낀 Python의 매력
처음 Python을 배울 때는 “이게 정말 프로그래밍 언어인가?” 싶을 정도로 간결했습니다. C++에서 10줄로 작성하던 코드가 Python에서는 2~3줄로 끝나는 경우가 많았죠. 특히 데이터 분석 프로젝트를 진행하면서 Pandas와 NumPy의 강력함을 체감했습니다. 엑셀로 몇 시간 걸리던 작업이 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]
# 정답: ['추움', '적당', '더움', '추움', '적당', '더움']
정답 보기
```python # 문제 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) ```심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「Python 컴프리헨션 | 리스트, 딕셔너리, 세트 컴프리헨션 완벽 정리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「Python 컴프리헨션 | 리스트, 딕셔너리, 세트 컴프리헨션 완벽 정리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Python 컴프리헨션: 리스트, 딕셔너리, 세트 컴프리헨션 완벽 정리. 리스트 컴프리헨션 (List Comprehension)부터 핵심 개념·패턴·실무 함정을 코드 예제로 풉니다. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. Python 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Python 예외 처리 | try-except, raise, 커스텀 예외 완벽 정리
- Python 데코레이터 | @decorator 완벽 정리
- Python 실전 시리즈 전체 목차 | #01~#23 학습 경로·영문 글·연관 글
- Python 자료형 | 리스트, 딕셔너리, 튜플, 세트 완벽 가이드
- Python 함수 | 매개변수, 반환값, 람다, 데코레이터 완벽 정리
이 글에서 다루는 키워드 (관련 검색어)
Python, 컴프리헨션, comprehension, 리스트, 딕셔너리, 제너레이터 등으로 검색하시면 이 글이 도움이 됩니다.