제너레이터

고봉진·2023년 4월 12일
0

python

목록 보기
3/4

제너레이터

📝 이터레이터를 생성해주는 함수. 발생자라고도 한다.

  • 함수 호출시 generator 객체가 반환된다. __iter__, __next__ 메서드를 갖고 있다.
  • yield 키워드를 사용해 값을 하나씩 반환하면서 소진되면 StopIteration 예외가 자동으로 발생한다. 이터레이터는 직접 발생시켜야 했다.
  • return 키워드로 함수 종료시에도 StopIteration 예외가 발생한다.
  • 제너레이터에 next() 함수 (또는 __next__ 메서드)를 사용해 값을 하나씩 불러오며, 제너레이터는 다음번 next() 실행시까지 대기한다.

제너레이터 만들기

def number_generator(stop):
    n = 0              # 숫자는 0부터 시작
    while n < stop:    # 현재 숫자가 반복을 끝낼 숫자보다 작을 때 반복
        yield n        # 현재 숫자를 바깥으로 전달
        n += 1         # 현재 숫자를 증가시킴
 
for i in number_generator(3):
    print(i)

이터레이터로 같은 기능을 수행하는 코드와 어떻게 다른지 눈여겨보자.


yield에서 함수 호출하기

def upper_generator(x):
    for i in x:
        yield i.upper()    # 함수의 반환값을 바깥으로 전달
 
fruits = ['apple', 'pear', 'grape', 'pineapple', 'orange']
for i in upper_generator(fruits):
    print(i)

return 키워드와 유사하게 사용할 수 있다.


yield from

아래의 두 코드는 기능적인 측면에서 동일하다:

def number_generator():
    x = [1, 2, 3]
    for i in x:
        yield i
 
for i in number_generator():
    print(i)
def number_generator():
    x = [1, 2, 3]
    yield from x    # 리스트에 들어있는 요소를 한 개씩 바깥으로 전달
 
for i in number_generator():
    print(i)

yield from에 제너레이터 객체 지정하기

제너레이터 또한 이터러블처럼 동작하므로(__iter__, __next__) 위 코드 x의 자리에 제너레이터를 지정할 수도 있다.

def number_generator(stop):
    n = 0
    while n < stop:
        yield n
        n += 1
 
def three_generator():
    yield from number_generator(3)    # 숫자를 세 번 바깥으로 전달
 
for i in three_generator():
    print(i)

📝 Generator Expression
아래와 같이 표현식을 괄호로 묶어 제너레이터를 만들수도 있다:

>>> x=(n*2 for n in range(10))
>>> type(x)
<class 'generator'>
>>> x
<generator object <genexpr> at 0x000002AA7DBD7648>

>>> next(x)
0
>>> next(x)
2
>>> next(x)
4

값을 하나씩 생성하므로 메모리를 절약할 수 있다.


병합 정렬

이제 병합 정렬에 도전해보자.

def merge_sort(collection: list) -> list:
    def merge(left: list, right: list) -> list:
        def _merge():
            while left and right:
                yield (left if left[0] <= right[0] else right).pop(0)
            yield from left
            yield from right

        return list(_merge())

    if len(collection) <= 1:
        return collection
    mid = len(collection) // 2
    return merge(merge_sort(collection[:mid]), merge_sort(collection[mid:]))

정리하면 아래와 같이 풀 수도 있을 것 같다(최소한 기능적인 측면에서):

def merge(left: list, right: list) -> list:
    def _merge():
		while left and right:
			yield (left if left[0] <= right[0] else right).pop(0)
		yield from left
		yield from right
    
    return list(_merge())


def merge_sort(collection: list) -> list:
    if len(collection) <= 1:
        return collection
    mid = len(collection) // 2
    return merge(merge_sort(collection[:mid]), merge_sort(collection[mid:]))

맨 아래 함수가 리스트를 받아서 정렬을 수행하는 주 함수이다. 리스트를 반으로 나눠 재귀하고 이에 대해 merge 함수를 실행한다. 제너레이터를 생성하는 _merge() 함수를 받아 리스트로 만드는 함수다. merge 함수의 두 인수를 받아 양쪽에 원소가 존재하는동안(while) 양 쪽의 맨 앞 원소를 비교해 적은 값을 yield하고, 한쪽이 빈다면 양쪽에서 yield를 받는다. next()는 StopIteration 예외를 발생시키니 yield from으로 처리한다. (yield는 값, yield from은 이터러블 객체로 기억하자.)

>>> x = (i for i in range(3))
>>> x.__next__
<method-wrapper '__next__' of generator object at 0x100e01e50>
>>> next(x)
0
>>> x.__next__()
1
>>> x.__next__()
2
>>> def numbers():
...    yield from x
...
>>> for i in numbers():
...     print(i)
...
>>>

이렇게 yield된 값을 모아서 리스트로 만들어 합치는 정렬이다.



참고자료

profile
이토록 멋진 휴식!

0개의 댓글