Effective Python Item 30. 리스트를 반환하기보다는 제너레이터를 사용하라

    들어가기 전

     


    요약

    • 제네레이터를 사용하면 결과를 리스트에 합쳐서 반환하는 것보다 더 깔끔함. 
    • 제네레이터를 사용하면, 결과를 리스트로 반환하는 것에 비해 적은 메모리로 작업 가능함. (OOM Kill 방지)
    • 처음 제네레이터 함수가 호출되면 이터레이터가 반환됨. 반환된 이터레이터를 next()로 호출할 때 마다 yield부터 다시 시작함. 
    • 제네레이터가 반환하는 이터레이터는 상태가 있기 때문에 재사용이 불가능하다는 것을 염두해야함. 

     


    Item 30. 리스트를 반환하기보다는 제너레이터를 반환하라.

    파이썬으로 코드를 작성할 때, 반환값을 위해서 빈 리스트를 선언하고 append()로 값을 추가해서 리스트를 반환하는 경우가 더러 있을 것이다. 그런데 이런 식으로 코드를 작성하는 것은 지양하는 것이 좋다고 한다. 그 이유는 다음과 같다. 

    • 빈 리스트를 선언해서 추가하는 작업을 하기 때문에 코드에 잡음이 섞임. (뭐가 중요한지 알 수 없을 것임) 
    • 리스트에 모든 데이터를 모아서 반환하기 때문에 그만큼 많은 메모리가 필요함.

    이런 이유 때문에 Result 리스트를 만들어서 반환하는 것보다는 제네레이터를 사용할 것을 권장한다. 제네레이터는 다음과 같이 사용할 수 있다.

    1. Yield 키워드를 포함한 함수를 제네레이터 함수라고 함. 
    2. 제네레이터 함수를 호출하면 이터레이터를 반환함.
    3. next()를 이용해 이터레이터를 호출하면 yield 문까지 진행 후 값을 반환하고, yield문부터 다시 시작함. 

    제네레이터를 사용하게 되면 다음과 같은 장점이 존재한다.

    1. 결과 리스트를 선언하고, 루프 문 안에서 결과 리스트에 값을 추가하는 연산을 하지 않아도 됨.
    2. 한 건씩 반환하기 때문에 메모리 사용량이 그만큼 감소함

    2번 동작이기 때문에 자칫 잘못하면 발생할 수 있는 OOM Killed 같은 문제를 예방할 수도 있게 된다. 이런 이유 때문에 결과 리스트를 반환하는 것보다는 제네레이터를 반환하는 것이 좋다. 


    코드 살펴보기

    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 = '컴퓨터(영어: Computer, 문화어: 콤퓨터 , 순화어: 전산기)는 진공관'
    result = index_words(address)
    print(result[:-1])

    단어가 시작되는 위치를 파악하는 함수 index_words()를 다음과 같이 선언했다. 이 함수는 다음에 주목해야 한다. 

    1. 사용하지 않는 결과 리스트를 우선 만든다.
    2. 만족하는 조건이 있을 때 마다 result.append()를 호출함.

    중요한 부분은 letter == ' ' 부분인데, 함수 내에서 result에 대한 참조가 더 많이 등장하면서 'result'라는 값에 더 많은 이목이 집중되어있다. 따라서 코드 자체에 잡음이 존재한다고 볼 수 있다. 뿐만 아니라 결과를 모은 후, 한번에 반환하기 때문에 이 메서드에서 사용해야하는 메모리의 크기가 그만큼 크다. 예상하지 못할만큼 큰 입력이 들어온다면, OOM이 발생할 수도 있을 것이다.

    def index_words(text):
        if text:
            yield 0
        for index, letter in enumerate(text):
            if letter == ' ':
                yield index + 1
    
    address = '컴퓨터(영어: Computer, 문화어: 콤퓨터 , 순화어: 전산기)는 진공관'
    it = index_words(address)
    
    print(next(it))
    print(next(it))
    print(next(it))

    위 코드는 리스트를 반환하는 대신 이터레이터를 반환하는 제네레이터 함수를 생성했다. 

    1. result가 없어져서, result에 대한 참조가 그만큼 제거되었따. 따라서 letter == ' ' 라는 문장에 더 집중할 수 있게 되었다. 
    2. 제네레이터 함수가 생성한 이터레이터를 next()로 호출할 때 마다, 변수가 하나씩 반환된다. 그만큼 메모리를 적게 사용하게 된다. 

    일반적으로 제네레이터에 익숙하지 않은 처음에는 오히려 가독성이 떨어진다고 생각할 수 있다. 그러나 불필요한 반환 리스트에 대한 참조가 줄어들어 로직에 더 집중할 수 있고, 메모리를 더 적게 사용할 수 있다는 장점이 존재한다. 

     


    코드2 : 제네레이터 + 파일 읽기 

    def index_file(handle):
        offset = 0
        for line in handle:
            if line:
                yield offset
            for letter in line:
                offset += 1
                if letter == ' ':
                    yield offset
    
    
    with open('address.txt', 'r') as f:
        it = index_file(f)
        results = itertools.islice(it, 0, 10)
        print(list(results))

    위 코드처럼 파일을 읽고 값을 파싱하는 작업도 제네레이터를 이용해 적은 메모리로 처리해 볼 수 있다. 그러나 이 경우, handle이라는 곳에 디스크 → 메모리로 버퍼링 된 값이 저장되어 있기 때문에 작업 메모리는 많이 필요하다고 볼 수 있다. 

    그러나 제네레이터를 호출하는 입장에서는 필요한 부분만 전달받는데, 그 관점에서는 리스트에 비해 적은 메모리를 쓴다고 볼 수 있다. 

    댓글

    Designed by JB FACTORY