C.W.K.
Stream
Lesson 02 of 07 · published

Generator — yield 가 __next__ 의 90% 를 대체

~22 min · generator, yield, lazy

Level 0호기심
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

일시정지 + 재개 가능한 함수

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 otherfor 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 가 필요한지, 아니면 한 원소씩 소비할지 봐. 후자면 appendyield 로. 메모리 덜, 시작 빨리, 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 종료 신호

External links

Exercise

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.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.