본문으로 건너뛰기
Advertisement

4.5 실전 팁 — 루프 성능 비교 & itertools 활용

대용량 데이터나 성능이 중요한 코드에서 루프 구현 방식의 선택은 성능에 큰 영향을 줍니다. 또한 Python 표준 라이브러리의 itertools 모듈은 메모리 효율적이고 빠른 이터레이터 연산을 제공합니다.


루프 성능 비교

timeit으로 측정하기

import timeit

N = 100_000

# 방법 1: for 루프 + append
def method_loop():
result = []
for x in range(N):
result.append(x * 2)
return result

# 방법 2: 리스트 컴프리헨션
def method_comprehension():
return [x * 2 for x in range(N)]

# 방법 3: map()
def method_map():
return list(map(lambda x: x * 2, range(N)))

# 방법 4: map() + 곱셈 메서드 (람다 없이)
def method_map_mul():
return list(map((2).__mul__, range(N)))

# 측정
runs = 100
results = {}
for name, func in [
("loop + append", method_loop),
("list comprehension", method_comprehension),
("map + lambda", method_map),
("map + method", method_map_mul),
]:
t = timeit.timeit(func, number=runs)
results[name] = t
print(f" {name:25s}: {t:.4f}s ({t/runs*1000:.2f}ms per run)")

# 일반적 결과 (환경에 따라 다름):
# list comprehension: 가장 빠름
# map + method: 비슷하거나 조금 빠름
# loop + append: 약 20~30% 느림
# map + lambda: 람다 호출 오버헤드로 비슷하거나 느림

성능 비교: 조건부 필터

import timeit

data = list(range(1, 100_001))

def filter_loop():
result = []
for x in data:
if x % 2 == 0:
result.append(x)
return result

def filter_comprehension():
return [x for x in data if x % 2 == 0]

def filter_builtin():
return list(filter(lambda x: x % 2 == 0, data))

def filter_modulo_method():
return list(filter((0).__eq__, map(lambda x: x % 2, data))) # 트릭

n = 50
t1 = timeit.timeit(filter_loop, number=n)
t2 = timeit.timeit(filter_comprehension, number=n)
t3 = timeit.timeit(filter_builtin, number=n)

print(f"루프: {t1:.3f}s")
print(f"컴프리헨션: {t2:.3f}s")
print(f"filter(): {t3:.3f}s")
# 컴프리헨션이 대체로 가장 빠름

NumPy와의 비교

# 수치 연산에서는 NumPy가 압도적으로 빠름
try:
import numpy as np
import timeit

N = 1_000_000

def pure_python():
return [x**2 for x in range(N)]

def numpy_version():
return np.arange(N) ** 2

t1 = timeit.timeit(pure_python, number=5)
t2 = timeit.timeit(numpy_version, number=5)
print(f"Pure Python: {t1:.3f}s")
print(f"NumPy: {t2:.3f}s")
print(f"속도 향상: {t1/t2:.1f}x")
# NumPy가 보통 10~100배 빠름

except ImportError:
print("NumPy가 설치되어 있지 않습니다: pip install numpy")

itertools 완전 정복

chain(): 여러 이터러블 연결

from itertools import chain

# 기본 chain
a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]

for val in chain(a, b, c):
print(val, end=" ") # 1 2 3 4 5 6 7 8 9
print()

# chain.from_iterable(): 중첩 이터러블 평탄화
nested = [[1, 2], [3, 4, 5], [6]]
flat = list(chain.from_iterable(nested))
print(flat) # [1, 2, 3, 4, 5, 6]

# 실전: 여러 파일의 내용을 하나로 처리
import io

files = [
io.StringIO("line1\nline2\n"),
io.StringIO("line3\nline4\n"),
]
all_lines = list(chain.from_iterable(f.readlines() for f in files))
print([line.strip() for line in all_lines])
# ['line1', 'line2', 'line3', 'line4']

islice(): 이터러블 슬라이싱

from itertools import islice

# 무한 이터레이터에서 일부만 가져오기
def infinite_counter(start=0, step=1):
while True:
yield start
start += step

# 처음 10개
first_10 = list(islice(infinite_counter(), 10))
print(first_10) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# 5부터 시작, 20까지, 2씩 건너뜀
sliced = list(islice(infinite_counter(), 5, 20, 2))
print(sliced) # [5, 7, 9, 11, 13, 15, 17, 19]

# 대용량 파일에서 처음 N줄만 읽기
import io

large_file = io.StringIO("\n".join(str(i) for i in range(1_000_000)))
first_100 = list(islice(large_file, 100))
print(f"읽은 줄 수: {len(first_100)}")

product(): 데카르트 곱

from itertools import product

# 기본: 두 이터러블의 모든 조합
colors = ["red", "green", "blue"]
sizes = ["S", "M", "L"]

for color, size in product(colors, sizes):
print(f"{color}-{size}", end=", ")
print()
# red-S, red-M, red-L, green-S, green-M, green-L, blue-S, blue-M, blue-L

# repeat 인자: 동일 이터러블을 n번 반복
# 예: 3자리 이진수 모두 생성
binary_3bit = list(product([0, 1], repeat=3))
print(binary_3bit)
# [(0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), (1,0,1), (1,1,0), (1,1,1)]

# 실전: 하이퍼파라미터 탐색
learning_rates = [0.001, 0.01, 0.1]
batch_sizes = [32, 64, 128]
epochs = [10, 20]

configs = [
{"lr": lr, "batch": bs, "epochs": ep}
for lr, bs, ep in product(learning_rates, batch_sizes, epochs)
]
print(f"탐색할 설정 수: {len(configs)}") # 18

combinations(), permutations()

from itertools import combinations, combinations_with_replacement, permutations

items = ["A", "B", "C", "D"]

# combinations: 순서 없는 조합 (중복 불허)
print("조합 (2개):")
for combo in combinations(items, 2):
print(combo, end=" ")
print()
# ('A', 'B') ('A', 'C') ('A', 'D') ('B', 'C') ('B', 'D') ('C', 'D')

# permutations: 순서 있는 순열
print("순열 (2개):")
for perm in permutations(items, 2):
print(perm, end=" ")
print()
# ('A', 'B') ('A', 'C') ... ('D', 'C') — 12개

# combinations_with_replacement: 중복 허용 조합
print("중복 조합 (2개):")
for combo in combinations_with_replacement(["A", "B", "C"], 2):
print(combo, end=" ")
print()
# ('A', 'A') ('A', 'B') ('A', 'C') ('B', 'B') ('B', 'C') ('C', 'C')

# 실전: 팀 조합 계산
players = ["Alice", "Bob", "Charlie", "Diana", "Eve"]
team_size = 2

teams = list(combinations(players, team_size))
print(f"가능한 팀 조합 수: {len(teams)}") # 10
for team in teams:
print(f" {' & '.join(team)}")

groupby(): 그룹화

from itertools import groupby

# 중요: groupby는 연속된 같은 키만 그룹화 — 정렬 필요!
data = [
{"category": "A", "value": 1},
{"category": "A", "value": 2},
{"category": "B", "value": 3},
{"category": "A", "value": 4}, # A가 다시 등장 — 정렬 안 하면 새 그룹
{"category": "B", "value": 5},
]

# 정렬 없이 groupby
print("정렬 없이:")
for key, group in groupby(data, key=lambda x: x["category"]):
values = [d["value"] for d in group]
print(f" {key}: {values}")
# A: [1, 2]
# B: [3]
# A: [4] ← 다시 A 그룹이 생김!
# B: [5]

# 정렬 후 groupby
print("정렬 후:")
sorted_data = sorted(data, key=lambda x: x["category"])
for key, group in groupby(sorted_data, key=lambda x: x["category"]):
values = [d["value"] for d in group]
print(f" {key}: {values}")
# A: [1, 2, 4]
# B: [3, 5]


# 실전: 로그 분석
logs = [
{"level": "INFO", "msg": "서버 시작"},
{"level": "INFO", "msg": "연결 수락"},
{"level": "WARNING", "msg": "메모리 부족"},
{"level": "ERROR", "msg": "DB 연결 실패"},
{"level": "INFO", "msg": "재시도"},
{"level": "ERROR", "msg": "타임아웃"},
]

sorted_logs = sorted(logs, key=lambda x: x["level"])
log_groups = {
level: [log["msg"] for log in group]
for level, group in groupby(sorted_logs, key=lambda x: x["level"])
}
print(log_groups)

accumulate(): 누적 연산

from itertools import accumulate
import operator

numbers = [1, 2, 3, 4, 5]

# 기본: 누적 합
cumsum = list(accumulate(numbers))
print(cumsum) # [1, 3, 6, 10, 15]

# 다른 연산자
cumprod = list(accumulate(numbers, operator.mul))
print(cumprod) # [1, 2, 6, 24, 120]

# 누적 최대값
data = [3, 1, 4, 1, 5, 9, 2, 6, 5]
cummax = list(accumulate(data, max))
print(cummax) # [3, 3, 4, 4, 5, 9, 9, 9, 9]

# 초기값 설정 (Python 3.8+)
with_initial = list(accumulate(numbers, initial=100))
print(with_initial) # [100, 101, 103, 106, 110, 115]

# 실전: 런닝 토탈 계산
daily_sales = [1200, 800, 1500, 900, 2100, 700, 1800]
running_total = list(accumulate(daily_sales))
print("일별 누적 매출:")
for day, (daily, total) in enumerate(zip(daily_sales, running_total), 1):
print(f" {day}일: {daily:,}원 (누적: {total:,}원)")

실전: 대용량 데이터 처리

from itertools import islice, chain, groupby
import io
import random

def generate_large_dataset(n: int):
"""대용량 데이터 시뮬레이션 — 제너레이터로 메모리 절약"""
categories = ["electronics", "clothing", "food", "books", "sports"]
for i in range(n):
yield {
"id": i,
"category": random.choice(categories),
"amount": round(random.uniform(1000, 100000), 2),
"region": random.choice(["Seoul", "Busan", "Incheon", "Daegu"]),
}

# 1. 데이터 스트림을 배치로 처리
def process_in_batches(data_stream, batch_size=1000):
"""대용량 스트림을 배치로 나누어 처리"""
data_iter = iter(data_stream)
while True:
batch = list(islice(data_iter, batch_size))
if not batch:
break
yield batch


# 2. 실전 파이프라인
print("대용량 데이터 처리 파이프라인:")
total_records = 0
category_totals = {}

dataset = generate_large_dataset(10_000)

for batch_num, batch in enumerate(process_in_batches(dataset, batch_size=500)):
# 각 배치에서 집계
for record in batch:
cat = record["category"]
category_totals[cat] = category_totals.get(cat, 0) + record["amount"]
total_records += 1

print(f"처리된 레코드: {total_records:,}개")
print("카테고리별 합계:")
for cat, total in sorted(category_totals.items()):
print(f" {cat}: {total:,.0f}원")


# 3. 여러 소스 데이터 통합
source1 = ({"source": "A", "value": i} for i in range(1, 6))
source2 = ({"source": "B", "value": i} for i in range(6, 11))
source3 = ({"source": "C", "value": i} for i in range(11, 16))

# chain으로 모든 소스 통합 — 메모리에 모두 올리지 않음
combined = chain(source1, source2, source3)
total = sum(d["value"] for d in combined)
print(f"\n통합 합계: {total}") # 120

고수 팁

1. 이터레이터 한 번만 순회 가능 — 주의!

from itertools import chain

gen = (x for x in range(5))
print(list(gen)) # [0, 1, 2, 3, 4]
print(list(gen)) # [] — 이미 소진됨!

# 재사용이 필요하면 list로 변환
from itertools import tee

gen = (x for x in range(5))
gen1, gen2 = tee(gen, 2) # 복사 (하지만 메모리 사용 주의)
print(list(gen1)) # [0, 1, 2, 3, 4]
print(list(gen2)) # [0, 1, 2, 3, 4]

2. itertools.takewhile / dropwhile

from itertools import takewhile, dropwhile

numbers = [1, 3, 5, 2, 4, 6, 7, 9]

# 조건이 True인 동안만 가져옴
odd_prefix = list(takewhile(lambda x: x % 2 == 1, numbers))
print(odd_prefix) # [1, 3, 5] — 2를 만나면 중단

# 조건이 True인 동안 건너뛰고 나머지
after_odd = list(dropwhile(lambda x: x % 2 == 1, numbers))
print(after_odd) # [2, 4, 6, 7, 9]

3. 성능 최적화 요약

방법적합한 상황메모리
for 루프복잡한 로직, 상태 유지O(n)
리스트 컴프리헨션간단한 변환/필터O(n)
제너레이터 표현식대용량 데이터, 일회성 순회O(1)
map/filter내장 함수 적용, 함수형 스타일O(1)
itertools복잡한 이터레이터 조합O(1)
NumPy수치 연산, 행렬O(n) but fast
Advertisement