파이써닉한 파이썬을 배워보자 - 6일차 제너레이터

0

pythonic

목록 보기
6/10

제너레이터(Generator)

제너레이터와 yield

함수에서 yield 키워드를 사용하면 제너레이터라는 객체를 정의하게 된다. 제너레이터의 주된 기능은 반복에 사용할 값을 생성하는 것이다. 다음의 예를 살펴보자.

def countdown(n):
    print('Counting down from', n)
    while n > 0:
        yield n
        n -= 1

c = countdown(10)
print(c)

위의 코드를 실행하면 다음의 결과를 얻는다.

<generator object countdown at 0x7fb0ce79c110>

제너레이터 객체를 얻게되는데, 이는 yield 키워드로 생성된 제너레이터 객체로 그 때 당시의 값을 어떻게 호출할 수 있는 지 방법을 가지고 있다. 제너레이터는 반복을 시작할 때 함수를 실행한다. 제너레이터 객체를 수행하는 방법은 다음과 같이 next()를 호출하는 것이다.

c = countdown(10)
print(next(c), next(c)) # 10 9

next를 호출하면 제너레이터 함수는 yield문까지 문장을 실행한다. yield문은 결과를 반환하며 next()가 다시 호출되기 전까지 함수 실행을 일시적으로 중단한다. 중단되는 동안 함수는 지역 변수와 실행 환경을 모두 유지한다. 함수가 재개되면 yield 다음에 오는 문장에서 실행을 계속한다.

next()는 제너레이터의 __next__()메서드를 호출하는 약어이다. 다음과 같이 수행도 가능하다.

c = countdown(10)
print(next(c), next(c), c.__next__(), c.__next__()) # 10 9 8 7

보통 제너레이터에서 next()를 직접 호출할 일은 없고, 항목을 처리하는 for문 또는 특정 연산에서 사용한다. 다음의 예를 살펴보자.

for n in countdown(10):
    ...
a = sum(countdown(10))

제너레이터 함수는 함수의 끝에 도달하거나 return문으로 반환되기 전까지 항목을 생성한다. for루프가 종료되면 StopIteration 예외가 발생한다. 제너레이터 함수가 None이 아닌 값을 반환한다면 StopIteration 예외와 함께 반환된다. 다음 예제 코드에서 제너레이터는 yieldreturn을 모두 사용한다.

def func():
    yield 37
    return 42

f = func()
print(next(f)) # 37
print(next(f)) # StopIteration: 42

반환값이 StopIteration과 함께 있는 것을 볼 수 있다. except로 예외를 걸러내고 값을 가져올 수 있다.

try:
    print(next(f)) # 37
except StopIteration as e:
    print(e.value) # 42

일반적으로 제너레이터 함수는 값을 반환하지 않는다. 제너레이터는 예외값(exception value)을 얻을 방법이 없는 for루프에서 대부분 처리된다. 이것은 값을 얻는 유일한 방법이 명시적으로 next()를 호출하여 수동으로 제너레이터를 실행하는 것 뿐임을 의미한다. 제너레이터와 관련된 대부분의 코드가 꼭 이렇진 않다.

제너레이터가 부분적으로만 실행되는 경우, 제너레이터에서 미묘한 문제가 발생한다. 가령 다음 예시는 반복에서 일찍 빠져나오는 코드이다.

def countdown(n):
    print('Counting down from', n)
    while n > 0:
        yield n
        n -= 1

for x in countdown(10):
    print(x)
    if x == 2:
        break

결과는 다음과 같다.

Counting down from 10
10
9
8
7
6
5
4
3
2

위 예에서 break가 호출되면 for루프가 중단되고 제너레이터는 끝까지 실행되지 않는다. 제너레이터에서 일종의 정리 작업을 수행하는 게 중요하다면 try-finally또는 컨텍스트 관리자를 사용해야 한다.

def countdown(n):
    print('Counting down from', n)
    try:
        while n > 0:
            yield n
            n -= 1
    finally:
        print("Only made it to", n)

for x in countdown(10):
    print(x)
    if x == 2:
        break

결과는 다음과 같다.

Counting down from 10
10
9
8
7
6
5
4
3
2
Only made it to 2

위 코드에서 제너레이터가 모두 실행되지 않아도 finally블록 코드는 실행이 보장된다. 버려진 제너레이터는 가비지 컬렉션 될 때 실행된다. 마찬가지로 컨텍스트 관리자와 정리 작업 코드도 제너레이터가 종료될 때 실행을 보장한다.

def func(filename):
    with open(filename) as file:
        ...
        yield data
        ...
    # 제너레이터가 버려져도 파일은 여기서 닫힘

자원을 적절히 정리하는 것은 까다로운 문제이다. try-finally 또는 컨텍스트 관리자와 함께 제너레이터를 사용하는 한, 제너레이터는 일찍 종료되더라도 올바른 작업을 수행한다.

계속 사용할 수 있는 제너레이터 만들기

일반적으로 제너레이터는 다음과 같이 한 번만 실행된다.

def countdown(n):
    while n > 0:
        yield n
        n -= 1

c = countdown(3)
for n in c:
    print(n)

for n in c:
    print(n)

결과는 다음과 같다.

3
2
1

두 번째 for문에는 나오는 것이 없다. 이는 제너레이터 객체가 종료되었기 때문이다.

여러 번 반복할 수 있는 객체가 필요하다면, 이를 클래스로 정의하고 __iter__() 메서드를 제너레이터로 만들면 된다.

class countdown:
    def __init__(self, start):
        self.start = start
    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

c = countdown(3)
for n in c:
    print(n)

for n in c:
    print(n)

결과는 다음과 같다.

3
2
1
3
2
1

제너레이터 위임(delegation)

yield를 포함하는 함수는 스스로 실행되지 않는다는 점이 제너레이터의 중요한 특징이다. 제너레이터는 항상 for-loop 또는 명시적으로 next()를 호출하는 다른 코드가 있어야 한다. 단순 일반 호출만으로 제너레이터를 실행하는 게 충분하지 않기 때문에 yield를 포함하는 라이브러리 함수는 작성하기가 다소 까다롭다. 디를 해결하기 위해 yield from문을 사용할 수 있다.

def countup(stop):
    n = 1
    while n <= stop:
        yield n
        n += 1

def countdown(start):
    n = start
    while n > 0:
        yield n
        n -= 1

def up_and_down(n):
    yield from countup(n)
    yield from countdown(n)

for x in up_and_down(5):
    print(x, end=" ") # 1 2 3 4 5 5 4 3 2 1

yeild from은 반복 프로세스를 외부 반복 객체에 효과적으로 위임한다.

1 2 3 4 5 5 4 3 2 1

yield from은 반복 작업을 직접 수행하지 않도록 해준다. 이 기능이 없으면 up_and_down(n)은 다음과 같이 작성해야한다.

def up_and_down(n):
    for x in countup(n):
        yield x
    for x in countdown(n):
        yield x

yield from은 중첩된 반복 가능 객체를 재귀적으로 반복해야하는 코드를 작성할 때 특히 유용하다. 다음은 리스트를 중첩없이 평평하게 flatten하는 코드이다.

def flatten(items):
    for i in items:
        if isinstance(i, list):
            yield from flatten(i)
        else:
            yield i

a = [1,2,[3,[4,5], 6 , 7], 8]
for x in flatten(a):
    print(x, end=" ") # 1 2 3 4 5 6 7 8

이 구현의 한 가지 제약 사항은 여전히 파이썬의 재귀 제한(recursion limit)이 적용되므로 깊이 중첩된 구조는 처리할 수 없다.

제너레이터는 다른 복잡한 사용이 있지만 이는 추후에 알아보도록 하자.

0개의 댓글