3.1 제너레이터 소개
제너레이터란 이터레이터 객체를 반환하는 함수야. 이 이터레이터들은 요청될 때마다 값을 생성하기 때문에 잠재적으로 큰 데이터를 다루면서도 메모리에 전부 올리지 않고 처리할 수 있어.
제너레이터를 만드는 방법은 여러 가지가 있어, 아래에 가장 인기 있는 방법들을 설명할게.
함수를 이용한 제너레이터
제너레이터는 함수 안에 yield
키워드를 사용해서 만들어져. yield
가 호출되면
제너레이터 객체를 반환하지만, 함수 내의 코드는 즉시 실행되지 않아. 대신, yield
표현식에서
실행이 중단되고, 제너레이터 객체의 __next__()
메소드가 호출될 때마다 실행이 재개돼.
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1
counter = count_up_to(5)
print(next(counter)) # 출력: 1
print(next(counter)) # 출력: 2
print(next(counter)) # 출력: 3
print(next(counter)) # 출력: 4
print(next(counter)) # 출력: 5
함수에 yield
연산자가 있는 경우, Python은 전통적인 함수 실행 대신 함수의 실행 상태를 관리하는 제너레이터 객체를 생성해.
제너레이터 표현식
제너레이터 표현식은 리스트 컴프리헨션(List Comprehension)과 비슷하지만, 대괄호 대신 소괄호를 사용해. 이들은 또한 제너레이터 객체를 반환해.
squares = (x ** 2 for x in range(10))
print(next(squares)) # 출력: 0
print(next(squares)) # 출력: 1
print(next(squares)) # 출력: 4
어떤 방법이 더 마음에 들어?
3.2 제너레이터의 장점
효율적인 메모리 사용
제너레이터는 값을 바로 계산해서 대량의 데이터를 메모리에 전부 올리지 않고 처리할 수 있어. 이는 대량의 데이터나 데이터 스트림을 처리하는 데 제너레이터가 적합한 이유야.
def large_range(n):
for i in range(n):
yield i
for value in large_range(1000000):
# 하나씩 값을 처리해
print(value)
지연 계산
제너레이터는 지연 계산을 수행하는데, 이는 필요할 때만 값을 계산한다는 의미야. 이를 통해 불필요한 계산을 피하고 성능을 향상시켜.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib))
편리한 문법
제너레이터는 이터레이터를 생성하기 위한 편리한 문법을 제공해, 이는 코드 작성과 읽기를 용이하게 만들어줘.
3.3 제너레이터 사용하기
표준 라이브러리에서 제너레이터 사용 예시
Python의 표준 라이브러리에는 제너레이터를 활용하는 많은 함수들이 있어. 예를 들어, 함수 range()
는
숫자 시퀀스를 생성하는 제너레이터 객체를 반환해.
for i in range(10):
print(i)
세상이 이제 다시는 예전처럼 되지 않을 거야.
무한 시퀀스 생성하기
제너레이터는 다양한 시나리오에서 유용하게 사용되는 무한 시퀀스를 생성할 수 있어, 예를 들어 무한 데이터 스트림을 생성할 때처럼.
def natural_numbers():
n = 1
while True:
yield n
n += 1
naturals = natural_numbers()
for _ in range(10):
print(next(naturals))
send()
와 close()
사용하기
제너레이터 객체는 send()
와 close()
메소드를 지원하는데, 이 메소드들은 제너레이터로 값을 보내거나 실행을 종료하는 데 사용돼.
def echo():
while True:
received = yield
print(received)
e = echo()
next(e) # 제너레이터 시작
e.send("Hello, world!") # 출력: Hello, world!
e.close()
3.4 제너레이터 실전 사용
제너레이터와 예외 처리
제너레이터는 예외를 처리할 수 있어서, 더욱 견고한 코드를 작성하는 데 강력한 도구가 돼.
def controlled_execution():
try:
yield "Start"
yield "Working"
except GeneratorExit:
print("제너레이터가 종료됨")
gen = controlled_execution()
print(next(gen)) # 출력: Start
print(next(gen)) # 출력: Working
gen.close() # 출력: 제너레이터가 종료됨
예외 처리에 대해서는 다음 강의에서 더 자세히 다룰 예정인데, 제너레이터가 예외 처리에 잘 작동한다는 걸 알면 좋겠지.
중첩 제너레이터
제너레이터는 중첩해서 사용할 수 있어서, 복잡한 반복 구조를 생성할 수 있어.
def generator1():
yield from range(3)
yield from "ABC"
for value in generator1():
print(value)
# 출력
0
1
2
A
B
C
설명:
yield from
: 이 구조는 다른 제너레이터의 일부 연산을 위임할 때 사용되며, 코드 간소화와 가독성을 높여줘.
제너레이터와 성능
제너레이터를 사용하면 메모리 사용을 줄이고 반복 실행을 더 효율적으로 하여 프로그램의 성능을 크게 향상시킬 수 있어.
리스트와 제너레이터의 비교 예제
import time
import sys
def memory_usage(obj):
return sys.getsizeof(obj)
n = 10_000_000
# 리스트 사용
start_time = time.time()
list_comp = [x ** 2 for x in range(n)]
list_time = time.time() - start_time
list_memory = memory_usage(list_comp)
# 제너레이터 사용
start_time = time.time()
gen_comp = (x ** 2 for x in range(n))
gen_result = sum(gen_comp) # 결과 비교를 위한 합계 계산
gen_time = time.time() - start_time
gen_memory = memory_usage(gen_comp)
print(f"리스트:")
print(f" 시간: {list_time:.2f} 초")
print(f" 메모리: {list_memory:,} 바이트")
print(f"\n제너레이터:")
print(f" 시간: {gen_time:.2f} 초")
print(f" 메모리: {gen_memory:,} 바이트")
리스트:
시간: 0.62 초
메모리: 89,095,160 바이트
제너레이터:
시간: 1.13 초
메모리: 200 바이트
GO TO FULL VERSION