본문으로 건너뛰기
Advertisement

4.3 for / while — 반복문 심화

반복문은 프로그래밍의 핵심 도구입니다. Python의 forwhile은 단순 반복을 넘어 이터레이터 프로토콜과 긴밀하게 연결되어 있습니다. 올바르게 사용하면 코드가 명확해지고 성능도 높아집니다.


for + range(): 완전 정복

# range(stop): 0부터 stop-1까지
for i in range(5):
print(i, end=" ") # 0 1 2 3 4
print()

# range(start, stop): start부터 stop-1까지
for i in range(2, 8):
print(i, end=" ") # 2 3 4 5 6 7
print()

# range(start, stop, step): step 간격
for i in range(0, 20, 3):
print(i, end=" ") # 0 3 6 9 12 15 18
print()

# 역순: step이 음수
for i in range(10, 0, -1):
print(i, end=" ") # 10 9 8 7 6 5 4 3 2 1
print()

# range는 메모리 효율적 — 실제 리스트를 만들지 않음
big_range = range(0, 10_000_000)
print(type(big_range)) # <class 'range'>
print(len(big_range)) # 10000000 — O(1) 연산

# range 객체 활용
r = range(1, 11)
print(5 in r) # True — O(1) 검사
print(r[3]) # 4 — 인덱스 접근
print(list(r[:3])) # [1, 2, 3] — 슬라이싱

enumerate(): 인덱스와 값 동시 접근

fruits = ["apple", "banana", "cherry", "date"]

# 나쁜 예: 수동 인덱스
for i in range(len(fruits)):
print(f"{i}: {fruits[i]}")

# 좋은 예: enumerate 사용
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
# 출력:
# 0: apple
# 1: banana
# 2: cherry
# 3: date

# start 파라미터로 시작 인덱스 지정
for i, fruit in enumerate(fruits, start=1):
print(f"{i}번: {fruit}")
# 출력:
# 1번: apple
# 2번: banana ...

# 실전: 리스트의 특정 원소 수정
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
for i, num in enumerate(numbers):
if num < 3:
numbers[i] = 0
print(numbers) # [3, 0, 4, 0, 5, 9, 0, 6]

zip(): 여러 이터러블 병렬 순회

names = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]
grades = ["A", "B", "A"]

# 기본 zip: 가장 짧은 이터러블 기준으로 중단
for name, score, grade in zip(names, scores, grades):
print(f"{name}: {score}점 ({grade})")

# zip_longest: 긴 이터러블 기준, 짧은 쪽은 fillvalue로 채움
from itertools import zip_longest

extra_names = ["Dave", "Eve"]
extra_scores = [78]

for name, score in zip_longest(extra_names, extra_scores, fillvalue=0):
print(f"{name}: {score}점")
# Dave: 78점
# Eve: 0점


# zip(strict=True) — Python 3.10+: 길이가 다르면 ValueError
try:
for a, b in zip([1, 2, 3], [4, 5], strict=True):
print(a, b)
except ValueError as e:
print(f"오류: {e}") # zip() 인자의 길이가 다릅니다


# 딕셔너리 생성에 zip 활용
keys = ["name", "age", "city"]
values = ["Alice", 30, "Seoul"]
person = dict(zip(keys, values))
print(person) # {'name': 'Alice', 'age': 30, 'city': 'Seoul'}

# 행렬 전치 (transpose)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transposed = list(zip(*matrix))
print(transposed) # [(1, 4, 7), (2, 5, 8), (3, 6, 9)]

for-else / while-else: 루프 정상 종료 감지

Python의 독특한 기능으로, for/while 루프가 break 없이 정상 종료되면 else 블록이 실행됩니다.

# for-else: break가 발생하지 않으면 else 실행
def find_prime_factor(n: int) -> int | None:
"""n의 첫 번째 소인수를 반환, 없으면 None"""
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return i # 소인수 발견 → else 실행 안 됨
else:
return None # break 없이 루프 완료 → n이 소수


for n in [12, 17, 25, 29]:
factor = find_prime_factor(n)
if factor:
print(f"{n} = {factor} × {n // factor}")
else:
print(f"{n}은 소수")


# while-else
def binary_search(arr: list, target: int) -> int:
"""이진 탐색. 찾으면 인덱스, 없으면 -1"""
left, right = 0, len(arr) - 1

while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
break
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
else:
return -1 # break 없이 종료 = 찾지 못함

return mid


sorted_arr = [1, 3, 5, 7, 9, 11, 13]
print(binary_search(sorted_arr, 7)) # 3 (인덱스)
print(binary_search(sorted_arr, 6)) # -1 (없음)

while + break 패턴: 무한 루프 탈출

import random

# 기본 무한 루프 패턴
attempt = 0
while True:
attempt += 1
value = random.randint(1, 10)
print(f"시도 {attempt}: {value}")
if value == 7:
print("7을 찾았습니다!")
break


# 재시도 패턴 (최대 횟수 제한)
MAX_RETRIES = 3

def fetch_data(url: str) -> dict | None:
"""네트워크 요청 시뮬레이션"""
import random
return {"data": "success"} if random.random() > 0.5 else None

url = "https://api.example.com/data"
result = None
retries = 0

while retries < MAX_RETRIES:
result = fetch_data(url)
if result is not None:
print(f"성공 (시도 {retries + 1}회)")
break
retries += 1
print(f"실패, 재시도 중... ({retries}/{MAX_RETRIES})")
else:
print(f"최대 재시도 횟수 초과")


# 입력 받기 패턴
def get_valid_age() -> int:
"""유효한 나이를 입력받을 때까지 반복 (시뮬레이션)"""
inputs = ["abc", "-5", "200", "25"] # 시뮬레이션용
idx = 0
while True:
user_input = inputs[idx]
idx = (idx + 1) % len(inputs)
print(f"입력: {user_input}")
try:
age = int(user_input)
if 0 <= age <= 150:
return age
print(" 나이는 0~150 사이여야 합니다")
except ValueError:
print(" 숫자를 입력하세요")

print(f"유효한 나이: {get_valid_age()}")

continue, break 활용 패턴

# continue: 현재 반복 건너뜀
data = [1, None, 3, None, 5, None, 7]

total = 0
for item in data:
if item is None:
continue # None이면 건너뜀
total += item
print(f"합계: {total}") # 16


# break: 루프 즉시 탈출
def first_negative(numbers: list[int]) -> int | None:
for n in numbers:
if n < 0:
return n # 함수 내에서는 break 대신 return
return None


# 중첩 루프 탈출 — break는 한 단계만 탈출
# 외부 루프까지 탈출하려면 플래그 변수나 함수화 사용
def find_in_matrix(matrix: list[list[int]], target: int) -> tuple[int, int] | None:
for i, row in enumerate(matrix):
for j, val in enumerate(row):
if val == target:
return (i, j) # 함수화로 중첩 루프 한 번에 탈출
return None


matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(find_in_matrix(matrix, 5)) # (1, 1)
print(find_in_matrix(matrix, 10)) # None

itertools 맛보기

import itertools

# chain(): 여러 이터러블을 하나로 연결
from itertools import chain

list1 = [1, 2, 3]
list2 = [4, 5, 6]
list3 = [7, 8, 9]

for item in chain(list1, list2, list3):
print(item, 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]


# cycle(): 무한 반복
from itertools import cycle, islice

colors = cycle(["red", "green", "blue"])
# islice로 무한 이터레이터 제한
for color in islice(colors, 7):
print(color, end=" ") # red green blue red green blue red
print()


# repeat(): 값을 n번 반복
from itertools import repeat

for val in repeat("hello", 3):
print(val, end=" ") # hello hello hello
print()

# zip과 repeat 조합
pairs = list(zip(range(5), repeat(0)))
print(pairs) # [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]

실전 예제: 중첩 루프로 행렬 연산

def matrix_multiply(a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
"""행렬 곱셈 (m×n) × (n×p) = (m×p)"""
m, n = len(a), len(a[0])
n2, p = len(b), len(b[0])

if n != n2:
raise ValueError(f"행렬 크기 불일치: ({m}×{n}) × ({n2}×{p})")

result = [[0.0] * p for _ in range(m)]

for i in range(m):
for j in range(p):
for k in range(n):
result[i][j] += a[i][k] * b[k][j]

return result


A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]
C = matrix_multiply(A, B)
for row in C:
print(row)
# [19.0, 22.0]
# [43.0, 50.0]

고수 팁

1. 루프 변수 재사용 주의

# 루프 후에도 변수가 남아 있음
for i in range(5):
pass
print(i) # 4 — 마지막 값이 남아 있음

# 의도치 않은 재사용 주의
result = [lambda: i for i in range(5)]
print([f() for f in result]) # [4, 4, 4, 4, 4] — 모두 마지막 i=4
# 올바른 방법: 기본값으로 캡처
result = [lambda i=i: i for i in range(5)]
print([f() for f in result]) # [0, 1, 2, 3, 4]

2. 루프 최적화: 속성 접근 캐싱

# 나쁜 예: 루프 내에서 매번 속성 접근
import math
result = []
for i in range(10000):
result.append(math.sqrt(i)) # math.sqrt를 매번 조회

# 좋은 예: 속성을 로컬 변수에 캐싱
sqrt = math.sqrt
result = []
for i in range(10000):
result.append(sqrt(i)) # 로컬 변수 조회가 더 빠름

3. 빈 이터러블에 안전한 루프

data = []   # 빈 리스트

# for는 빈 이터러블에 자동으로 안전
for item in data:
print(item) # 아무것도 출력 안 됨 — 오류 없음

# 첫 번째 원소가 필요할 때
first = next(iter(data), None) # None: 기본값
print(first) # None
Advertisement