본문 바로가기

Computer/Python

제너레이터와 yield (1)

반응형

파이썬 코딩을 하다보면 시퀀스를 결과로 출력하는 일이 많습니다. 이럴때 가장 간단한 선택은 담길 원소들이 저장된 리스트를 반호나하는 것이죠. 예를 들어 문자열에서 띄어쓰기의 인덱스를 반환하고 싶다면 다음 코드와 같이 리스트의 append 메소드를 사용해 리스트에 결과를 추가하고 함수 마지막에 리스트를 반환하면 됩니다.

def index_words(text):
    result = []
    if text:
        result.append(0)
    for index, letter in enumerate(text):
        if letter == ' ':
            result.append(index+1)
    return result

address = 'I am a student and a male and looking-good'
result = index_words(address)
result

하지만 이 코드는 잘 작동하지만 새로운 결과를 찾을 때마다 append 메소드를 호출하므로 중요한 리스트의 추가될 값이 바로 눈에 띄지 않으며, 최종적으로 반환하기 전에 리스트에 모든 결과를 다 저장해야된다는 점입니다. 따라서 입력이 매우 크면 메모리를 소진해서 중단될 수 있습니다. 이러한 점을 개선하는 방법은 파이썬의 yield 식을 사용하는 제너레이터 함수를 사용하는 것입니다.

def index_words_iter(text):
    if text:
        yield 0
    for index, letter in enumerate(text):
        if letter == ' ':
            yield index + 1    

it = index_words_iter(address)
next(it)
next(it)

이 함수가 호출되면 제너레이터 함수가 실제로 실행되지 않고 이터레이터라는 반복 가능한 객체를 반환합니다. 이터레이터가 next 내장 함수를 호출할 때마다 이터레이터는 제너레이터 함수를 다음 yield 까지 진행시키고 제너레이터가 yield 에 전달하는 값은 이터레이터에 의해 호출하는 쪽에 반환합니다.

반환하는 리스트가 따로 필요없으므로 함수 자체가 매우 읽기 쉬워지고 결과가 yield 식에 의해 전달되기는 하지만 제너레이터가 반환하는 이터레이터를 리스트 내장 함수에 넘기면 필요할 때 제너레이터를 리스트로 쉽게 변환이 가능합니다. 또한, 같은 기능을 하는 함수더라도 제너레이터 함수로 구현하면 리스트에 모든 결과값을 담지 않아도 되기에 사용하는 메모리 크기를 어느 정도 제한할 수 있습니다. 다음 코드는 파일에서 한 번에 한 줄씩 읽어 한 단어씩 출력하는 제너레이터 코드인데 이 함수의 작업 메모리는 입력 중 가장 긴 줄의 길이로 제한됩니다.

def index_file(handle):
    offset = 0
    for line in handle:
        if line:
            yield offset
        for letter in line:
            offset += 1
            if letter == ' ':
                yield offset                

이것을 연장하면 리스트 컴프리헨션에 대해서도 비슷하게 적용할 수 있습니다. 리스트 컴프리헨션은 입력 시퀀스와 같은 수의 원소가 들어 있는 리스트 객체를 만들어내므로 입력이 커지면 메모리를 상당히 많이 사용하게 됩니다. 이 경우에는 다음과 같이 리스트 컴프리헨션과 제너레이터를 일반화한 제너레이터 식을 이용할 수 있습니다. 이것도 마찬가지로 출력 시퀀스가 실체화되지 않고 이터레이터를 반환합니다.

a = [[1,2,3,4,5,6,7,8,9,10],[1,2,3,4,5,6,7],[1,2,3,4,5],[1,2,3],[1]]
it = (len(x) for x in a)
>>> it
<generator object <genexpr> at 0x7f1d64cd5ed0>

이것도 마찬가지로 next 내장 함수를 사용하여 다음 값을 가져올 수 있습니다. 특히 강력한 점은 두 제너레이터 식을 합성할 수 있다는 것입니다. 위 코드의 'it' 변수는 제너레이터로부터 반환된 이터레이터이므로 반복가능하므로 이 변수에 대하여 또다른 제너레이터를 정의할 수 있습니다. 즉, 앞의 제너레이터가 반환한 이터레이터를 다음과 같이 다른 제너레이터 식의 입력으로 사용하는 것으로 이 이터레이터를 전진시킬 때마다 내부의 이터레이터도 전진되면서 연쇄적으로 루프가 실행됩니다.

roots = ((x, x**0.5) for x in it)
print(next(roots))

제너레이터 사용 시 주의할 점은 이터레이터가 모든 원소를 소진하게 되면 재사용이 불가능하다는 점입니다. next 내장 함수로 모든 원소를 호출하면 다음과 같에 StopIteration 에러가 발생합니다. 제너레이터를 재사용하기 위해서는 제너레이터로부터 새로운 이터레이터를 다시 정의해주어야 합니다.

 


제너레이터와 yield (2) - 피보나치, 무한급수

제너레이터와 yield (3) - 지연 계산

반응형

'Computer > Python' 카테고리의 다른 글

스레드 세이프 (Thread-safe)  (0) 2021.06.20
GIL (Global Interpreter Lock), Multi-Threading  (0) 2021.06.20
Numba (1)  (0) 2021.06.02
Pycharm - Python Interpreter  (0) 2021.05.11
예외 처리에서의 동작 수행 - try/except/else/finally  (0) 2021.05.04