파이썬 클린 코드 - 제네레이터 개요

제네레이터 개요

대규모의 구매 기록에서 최저 판매가, 최고 판매가, 평균 판매가를 계산하고 싶다고 하자.

<purchase_data> , <price>

이런 구매 기록을 받아 필요한 지표를 구해주는 객체를 만든다고 할때, 최솟값이나 최댓값 같은 지표는 min(), max() 같은 내장함수로 쉽게 구할 수 있다. 하지만 어떤 지표는 모든 기록을 다 계산해서 얻을 수 있다. 구현하는 방법을 생각해보자면 간단하게 for 루프에서 각 단계에서 각 지표를 업데이트 하는 방법을 생각해보자.


class PurchasesStats:
    def __init__(self, purchases):
        self.purchases = iter(purchases)
        self.min_price: float = None
        self.max_price: float = None
        self._total_purchases_price: float = 0.0
        self._total_purchases = 0
        self._initialize()

    def _initialize():
        try:
            first_value = next(self.purchases)
        except StopIteration:
            raise ValueError("no values provided")

        self.min_price = self.max_price = first_value
        self._update_avg(first_value)

    def process(self):
        for purchase_value in self.purchases:
            self._update_min(purchase_value)
            self._update_max(purchase_value)
            self._update_avg(purchase_value)
        return self

    def _update_min(self, new_value: float):
        if new_value < self.min_price:
            self.min_price = new_value

    def _update_max(self, new_value: float):
        if new_value > self > max_price:
            self.max_price = new_value

    @property
    def avg_price(self):
        return self._total_purchases_price / self._total_purchases

    def _update_avg(self, new_value: float):
        self._total_purchases_price += new_value
        self._total_purchases += 1

    def __str__(self):
        return (
            f"{self.__class__.__name}({self.min_price},"
            f"{self.max_price, {self.avg_price}})"
        )
           

이제 이 모든 정보를 로드해서 담아서 반환해주는 함수를 만들어 보기

리스트에 불러오기

def _load_purchases(file_name):
    purchases = []
    with open(filename) as f:
        for line in f:
            *_, price_raw = line.partition(",")
            purchases.append(float(price_raw))
            
    return purchases

정확한 결과를 반환하지만, 파일에서 모든 정보를 읽어 list에 저장하기 떄문에
로드하는데 시간이 오래 걸리며 메모리 용량을 넘을 수도 있다.

결과는 정확함. 하지만 _load_purchases에서 다 읽어와 list에 저장하고
다 읽어온 데이터를 다시 for문으로 돌리기 때문에 비효율적

  • load해서 list에 저장하는 단계에서 시간과 메모리 둘 다 많이 필요함.

  • 앞에서 계산하는 코드는 한번에 하나의 데이터만을 사용함

-> 굳이 모든 데이터를 모두 읽어 메모리에 보관할 이유 x

해결책: 제네레이터를 만들어 필요한 값을 그때 그때 만들기

또한 앞에서 계산하는 코드는 한 번에 하나의 데이터만을 사용하고 있다.
그러므로 굳이 모든 데이터를 한 번에 모두 읽어 메모리에 보관할 이유가 없다.

해결책은 제네레이터를 만드는 것이다. 파일의 전체 내용을 리스트에 보관하는 대신
필요한 값을 그때 그때 가져오는 것이다. 다음과 같이 코드를 수정한다.

제네레이터로 효율적

def load_purchases(file_name):
    with open(filename) as f:
        for line in f:
            *_, price_raw = line.partition(",")
            yield float(price_raw)

메모리를 많이 필요하던 리스트 사라지고 return도 사라짐

이때의 load_purchases 함수를 제네레이터 함수, 또는 단순히 제네레이터라고 함.

-> 파이썬에선 어떤 함수라도 yield 키워드를 쓰면 제네레이터 함수가 된다.

yield가 포함된 이 함수를 호출하면 제네레이터의 인스턴스를 만듬

>>> load_purchases("purchases.csv")

<generator object load_purchases at 0x000001A888D422C8>

모든 제네레이터 객체는 이터러블입니다. (이터러블은 뒤에서 더 다룹니다.)

여기서 중요한 것은 제네레이터로 바꿨지만

함수의 사용 코드가 그대로 라는것!

즉 이터러블을 사용하면 for 루프의 다형성을 보장하는 강력한 추상화가 가능함!

-> 이터러블 인터페이스를 따르면 투명하게 객체의 요소를 반복하는 것이 가능하다

제네레이터 표현식

제네레이터는 이터레이터이므로 리스트나 튜플, 세트처럼 많은 메모리를 필요로 하는 이터러블이나 컨테이너의 대안이 될 수 있음.

제네레이터 컴프리헨션이라고 불러야한다는 주장도 있지만 제네레이터 표현식이 일반적으로 쓰임

[x** for x in range(10)]  #리스트 컴프리헨션 

(x**2 for x in range(10))  # 제네레이터 표현식(컴프리헨션)

sum(x**2 for x in range(10)) # 이터러블 연산이 가능한 함수에 제네레이터 표현식을 직접 전달 가능함.

좋은 웹페이지 즐겨찾기