generator 함수는 일반 함수처럼 보이는데 안에 yield 가 있어. 호출해도 본문이 안 돌아 — generator 객체 (iterator) 반환. next() 호출마다 본문이 다음 yield 까지 돌고, 그 값 반환하고 거기서 일시정지. 다음 next() 가 정확히 그 자리에서 재개, 모든 local state 보존. 함수가 return (또는 끝에 도달) 하면 StopIteration.
왜 generator 가 __iter__/__next__ 를 자주 대체하나
lesson 1 의 CountDown 클래스는 8 줄 + 보일러플레이트. generator 버전은 3 줄. 같은 동작, 훨씬 적은 코드. 단점은 iterator 에 추가 메서드 못 붙임 — 근데 거의 필요 없어. iterator use case 의 90% 는 generator 함수가 정답.
Lazy 평가 — 핵심 기능
generator 는 *물어볼 때만* 값 계산. def big_range(): yield from range(10**10) 가 100 억 숫자 메모리 안 쓰고. any(x > 100 for x in big_range()) 가 첫 매치에서 멈춰 — 전체 시퀀스 절대 만들지 않고. lazy 반복이 Python 을 streaming, 무한 시퀀스, 데이터베이스 없이도 가능한 파이프라인에 실용적으로 만들어.
yield from — 다른 iterable 에 위임
yield from other 는 for x in other: yield x 의 단축형 + send/throw/리턴 값 깔끔히 forward. 주 용도 — generator 합성. 한 generator 가 다른 generator 에서 yield 하고 싶으면 yield from.
send / throw / close — generator 를 coroutine 으로
generator 도 값 받을 수 있어. g.send(value) 가 value 를 마지막 yield 의 결과로 generator 재개. g.throw(ExcType) 가 일시정지 지점에 예외 raise. g.close() 는 거기 GeneratorExit raise. 한때 Python 의 유일한 coroutine 방법 — async/await 가 인수받기 전. 지금은 적게 보이지만 알아둬.
원칙: list 만들고 반환하는 함수가 있으면 — caller 가 진짜 전체 list 가 필요한지, 아니면 한 원소씩 소비할지 봐. 후자면 append 를 yield 로. 메모리 덜, 시작 빨리, caller 코드 동일.
Code
Generator 기본 — yield, 그 다음 일시정지·python
def countdown(n):
while n > 0:
yield n
n -= 1
for x in countdown(3):
print(x)
# 3
# 2
# 1
# CountDown 클래스랑 같은 동작 — 8 줄 대신 3 줄
print(list(countdown(5))) # [5, 4, 3, 2, 1]
print(sum(countdown(10))) # 55
Lazy 평가 — 무한 시퀀스·python
def naturals():
n = 1
while True:
yield n
n += 1
# 전부 반복 X — 처음 5 만
import itertools
first_5 = list(itertools.islice(naturals(), 5))
print(first_5) # [1, 2, 3, 4, 5]
# 답 찾자마자 멈춤
result = next(x for x in naturals() if x * x > 1000)
print(result) # 32 (32*32 = 1024)
yield from — 위임·python
def first_half(items):
yield from items[:len(items)//2]
def whole(items):
yield from first_half(items)
yield "middle marker"
yield from items[len(items)//2:]
for x in whole([1, 2, 3, 4, 5, 6]):
print(x)
# 1
# 2
# 3
# middle marker
# 4
# 5
# 6
Streaming — 큰 파일 통째 안 로드하고 처리·python
def lines_of(path):
with open(path) as f:
for line in f:
yield line.rstrip("\n")
# 메모리 비용은 한 줄, 파일 크기 무관
# (실제 예제는 진짜 파일 가리킴)
# for line in lines_of("huge.log"):
# if "ERROR" in line:
# print(line)
# 필터 파이프라인 자연스럽게 합쳐짐
def errors_only(lines):
for line in lines:
if "ERROR" in line:
yield line
# errors_only(lines_of("huge.log")) # 여전히 streaming, list 절대 안 만들어짐
send / throw — generator 를 coroutine 으로 (옛 스타일)·python
def echo():
while True:
received = yield
print("got:", received)
g = echo()
next(g) # prime — 첫 yield 까지 진행
g.send("hello") # 'got: hello'
g.send("world") # 'got: world'
g.close() # generator 종료 신호
Generator take_until(predicate, iterable) 작성 — predicate(item) 이 True 인 첫 item *전까지* yield, 그 item 은 포함 X, 거기서 멈춤. (a) take_until(lambda x: x > 5, range(100)) — 0, 1, 2, 3, 4, 5 yield. (b) 위 naturals() 무한 generator 와 결합해서 작동 확인. (c) 작은 chain(*generators) 에 yield from 써서 여러 generator 를 하나로.
Progress
Progress is local-only — sign in to sync across devices.