for 루프의 내부 동작인 이터레이터 프로토콜을 이해하고 enumerate·zip을 마스터한다.
🎯 이 강의에서 배우는 것
파이썬의 for 루프는 C 언어의 for 루프와 근본적으로 다릅니다. 파이썬의 for는 이터레이터 프로토콜(iterator protocol)을 기반으로 동작합니다. 이 원리를 이해하면 왜 for가 리스트, 문자열, 파일, 제너레이터 등 모든 것에 동작하는지 알 수 있습니다.
🔄 for 루프의 내부 동작
# 이 코드가 내부적으로 어떻게 동작하는가:
for x in [1, 2, 3]:
print(x)
# 실제로 일어나는 일:
_iter = iter([1, 2, 3]) # 1. __iter__() 호출 → 이터레이터 생성
while True:
try:
x = next(_iter) # 2. __next__() 호출 → 다음 값
print(x) # 3. 루프 본문 실행
except StopIteration: # 4. 더 없으면 StopIteration → 루프 종료
break
# iter()와 next()로 수동 이터레이션
lst = [10, 20, 30]
it = iter(lst) # 이터레이터 생성
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
try:
print(next(it)) # StopIteration!
except StopIteration:
print("다 꺼냈음")
🔍 이터러블 vs 이터레이터
# 이터러블(Iterable): __iter__() 메서드를 가진 객체
# for 문에 쓸 수 있음
# list, str, dict, set, tuple, range, 파일 객체 등
# 이터레이터(Iterator): __iter__() + __next__() 모두 가진 객체
# 이터러블에서 iter()로 생성
# 한 번 소진되면 재사용 불가
# 리스트는 이터러블이지만 이터레이터가 아님
lst = [1, 2, 3]
print(hasattr(lst, "__iter__")) # True — 이터러블
print(hasattr(lst, "__next__")) # False — 이터레이터 아님
# 리스트에서 이터레이터 생성
it = iter(lst)
print(hasattr(it, "__iter__")) # True
print(hasattr(it, "__next__")) # True — 이터레이터
# 중요: 이터레이터는 한 번만 순회 가능!
it = iter([1, 2, 3])
for x in it:
print(x) # 1, 2, 3
for x in it: # 이미 소진됨
print(x) # 아무것도 출력 안 됨!
# 반면 이터러블은 몇 번이든 순회 가능
for x in lst:
print(x) # 1, 2, 3
for x in lst:
print(x) # 또 1, 2, 3
📏 range() 객체 — Lazy 시퀀스
import sys
# range는 숫자를 미리 만들지 않음 — lazy 객체
r = range(1_000_000_000) # 10억짜리 range, 즉시 생성!
print(sys.getsizeof(r)) # 48 bytes (크기와 무관하게 일정)
print(sys.getsizeof(list(range(1000)))) # 8056 bytes (실제 리스트)
# range 매개변수
for i in range(5): # 0, 1, 2, 3, 4
print(i, end=" ")
print()
for i in range(1, 6): # 1, 2, 3, 4, 5
print(i, end=" ")
print()
for i in range(0, 10, 2): # 0, 2, 4, 6, 8 (step=2)
print(i, end=" ")
print()
for i in range(10, 0, -1): # 10, 9, 8, ..., 1 (역순)
print(i, end=" ")
print()
# range의 멤버십 테스트는 O(1)
print(999999 in range(1_000_000)) # True, 즉시!
# 리스트였다면 O(n) 순차 탐색
🔢 enumerate() — 인덱스와 값을 동시에
fruits = ["사과", "바나나", "체리"]
# 나쁜 방법: 인덱스 수동 관리
i = 0
for fruit in fruits:
print(f"{i}: {fruit}")
i += 1
# 파이썬다운 방법: enumerate()
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
# 0: 사과
# 1: 바나나
# 2: 체리
# start 파라미터: 시작 번호 지정
for num, fruit in enumerate(fruits, start=1):
print(f"{num}번: {fruit}")
# 1번: 사과
# 2번: 바나나
# 3번: 체리
# enumerate 내부 원리: 카운터를 가진 이터레이터
it = enumerate(["a", "b", "c"])
print(next(it)) # (0, 'a')
print(next(it)) # (1, 'b')
🤐 zip() — 여러 이터러블 병렬 순회
names = ["홍길동", "이순신", "강감찬"]
scores = [85, 92, 78]
grades = ["B", "A", "C"]
# zip: 여러 이터러블을 동시에 순회
for name, score, grade in zip(names, scores, grades):
print(f"{name}: {score}점 ({grade})")
# 홍길동: 85점 (B)
# 이순신: 92점 (A)
# 강감찬: 78점 (C)
# 가장 짧은 이터러블에서 멈춤
a = [1, 2, 3]
b = [10, 20]
for x, y in zip(a, b):
print(x, y) # (1,10), (2,20) — 3은 무시됨
# itertools.zip_longest: 긴 것에 맞춰 None으로 채움
from itertools import zip_longest
for x, y in zip_longest(a, b, fillvalue=0):
print(x, y) # (1,10), (2,20), (3,0)
# zip 활용 패턴
keys = ["name", "age", "city"]
values = ["홍길동", 30, "서울"]
person = dict(zip(keys, values))
print(person) # {'name': '홍길동', 'age': 30, 'city': '서울'}
# 전치행렬 (zip(*matrix))
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 루프와 시간 복잡도
# 이중 루프: O(n^2)
n = 4
for i in range(n):
for j in range(n):
print(f"({i},{j})", end=" ")
print()
# 딕셔너리로 O(n^2)→O(n) 최적화
# 두 수의 합(Two Sum) 문제
def two_sum_slow(nums, target):
# O(n^2): 모든 쌍을 확인
for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
return []
def two_sum_fast(nums, target):
# O(n): 딕셔너리로 이미 본 값 저장
seen = {} # {값: 인덱스}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
import time
nums = list(range(10000)) + [9999, 1]
target = 20000
start = time.perf_counter()
two_sum_slow(nums, target)
print(f"O(n^2): {time.perf_counter()-start:.4f}초")
start = time.perf_counter()
two_sum_fast(nums, target)
print(f"O(n): {time.perf_counter()-start:.4f}초")
⚠️ 자주 하는 실수
- for 루프에서 리스트를 수정: 순회 중인 리스트를 수정하면 예상치 못한 결과. 복사본을 순회하거나 리스트 컴프리헨션을 사용하세요.
- 이터레이터를 두 번 순회: 이터레이터는 소진되면 빈 루프.
list()로 변환하거나 다시iter()로 생성하세요. - 인덱스 수동 관리:
i = 0; for x in lst: ...; i += 1대신enumerate()를 사용하세요.
📝 정리 및 다음 강의 예고
- for 루프는 내부적으로 iter() → next() → StopIteration 패턴으로 동작합니다.
- 이터러블(반복 가능)과 이터레이터(한 번 순회)의 차이를 명확히 이해하세요.
enumerate()와zip()으로 인덱스를 수동 관리하지 않아도 됩니다.
다음 강의: 루프 제어 키워드 break, continue, pass와 파이썬 고유의 for-else/while-else 패턴을 완전히 이해합니다.
관련 주제
- 이터레이터 프로토콜
- iter()·next()·StopIteration
- range() lazy 시퀀스
- enumerate()
- zip() 병렬순회
- 중첩 for 루프 복잡도
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
