[Effective Python] BW 32. 긴 리스트 컴프리헨션보다는 제너레이터 식을 사용하라

전민수·2023년 6월 1일
0

EffectivePython

목록 보기
7/10

List 컴프리헨션의 단점

List 컴프리헨션은 반복되거나 특정 조건을 만족하는 시퀀스를 생성하기에 좋은 방법입니다. 간결하면서 가독성이 뛰어나죠.

하지만 입력 데이터의 크기가 커지면 문제가 발생합니다.
list를 정의하는 시점에 모든 데이터를 가지고 있는 Sequental data structure를 생성하기 때문에 데이터 크기가 매우 크다면, 프로그램에 할당된 메모리를 초과할 수 있으며, 이 경우 프로그램이 중단될 수 있습니다.

예를 들어, 2TB의 비디오 데이터셋을 딥러닝 모델에 학습하려고 합니다. 그럼 2TB의 비디오 데이터를 한번에 불러와 프로그램에 저장하고, 하나씩 꺼내 학습을 한다면 이게 가능할까요? 이렇게 되면 프로그램에 2TB 이상의 용량이 할당되어야 할 것 입니다.

Generator Expression

이런 메모리 문제를 해결할 수 있는 방법이 Generator 입니다.

제너레이터(Generator)는 iterator를 생성해주는 함수입니다. 이 함수에 의해 생성된 객체는 가지고 있는 정보에 따라 데이터를 생성합니다. 즉 이 객체가 현재 가지고 있는 데이터는 생성 규칙이라는 말이죠. 따라서 생성할 데이터가 굉장히 많아도 제너레이터 객체의 용량은 정해져 있습니다.

그래서 빅데이터라 할 지라도 제너레이터를 이용하면 처리할 수 있습니다.

사용자 함수에서는 return 대신 yield를 활용하면 제너레이터 객체를 반환할 수 있습니다.

제너레이터를 Expression으로 생성할 수 있습니다. 리스트 컴프리헨션과 제너레이터를 일반화한 것입니다.

# list comprehension
list_com = [x for x in range(5)]
# generator expression
gen_ex = (x for x in range(5))

위 두 코드는 모두 0,1,2,3,4 라는 데이터를 가지는 Sequential data를 정의했습니다. 첫 번째 코드는 list comprehension, 두 번째 코드는 gernerator expression을 이용했습니다.

두 객체의 크기을 한 번 살펴볼까요?

import sys

print(sys.getsizeof(list_com))
# 120
print(sys.getsizeof(gen_ex))
# 200

여기서는 전체 데이터 개수가 적어 오히려 제너레이터의 크기가 더 크군요.

그럼 데이터 개수를 10,000개로 해봅시다.

list_com = [x for x in range(10000)]
gen_ex = (x for x in range(10000))

print(sys.getsizeof(list_com))
# 85176
print(sys.getsizeof(gen_ex))
# 200

list의 경우, 데이터의 개수가 증가함에 따라 list의 크기도 증가했습니다.
하지만 generator의 경우, 데이터 개수가 5개일 때와 동일한 200 바이트입니다.

참고 : 실행속도는 리스트가 더 빠릅니다. 실험적으로 봤을 때 3배 정도 빠르다고 합니다. / 출처 : https://zephyrus1111.tistory.com/313

제너레이터의 합성

제너레이터 식에 제너레이터를 대입할 수 있습니다.

벡터 연산과 같이 각 iterator에 해당연산을 수행할 수 있습니다.

square_gen_ex = ((x, x**2) for x in gen_ex)

print(next(square_gen_ex))
# (0, 0)
print(next(square_gen_ex))
# (1, 1)
print(next(square_gen_ex))
# (2, 4)

0~99,999 사이의 정수를 iteration하는 gen_ex 제너레이터 객체를 제너레이터 식에 대입하여 square_gen_ex 객체를 생성했습니다.

제너레이터 합성을 단계 별로 들여다 보겠습니다.

square_gen_ex가 iteration 되면 gen_ex부터 iteration 되고, 그 값이 square_gen_ex의 expression에 대입됩니다.
주의 깊게 봐야할 부분은 iter의 진행상황 입니다.
제너레이터의 특성상 단 한번만 해당 iter의 값을 생성합니다.

그런데 만약 square_gen_ex가 iteration 되기 전에 gen_ex가 iteration 됐다면 어떻게 될까요?

gen_ex = (x for x in range(10000))

print(next(gen_ex))
print(next(gen_ex))
print(next(gen_ex))
# 0
# 1
# 2

square_gen_ex = ((x, x**2) for x in gen_ex)

print(next(square_gen_ex))
print(next(square_gen_ex))
print(next(square_gen_ex))

# (3, 9)
# (4, 16)
# (5, 25)
	

이렇게 gen_ex가 iteration 한 다음 부분부터 대입식에 대입됩니다.

제너레이터 합성은 최고효율로 메모리를 사용하면서 아주 빠른 속도로 연산할 수 있는 강력한 기능입니다. 위에 설명한 제너레이터의 특성만 유의해서 사용한다면, time & space 관점에서 모두 효율적인 코드를 만들 수 있을 것입니다.

profile
Learning Mate

0개의 댓글