이터레이터(Iterator)와 제너레이터(Generator)는 알고도 가끔 헷갈릴 때가 있습니다.
먼저, 결론부터 말하자면 다음과 같습니다.
- iterable 객체 : `iter` 함수에 인자로 전달이 가능한 반복 가능한 객체 ex. List, Dict, Set, String, range()
- iterator(이터레이터) : 그 반복 가능한 객체를 `iter` 함수에 인자로 전달해 순회할 수 있는 객체
- generator(제너레이터) : Iterator 객체를 만들 수 있는 함수 (메모리 절약!)
어떤 의미인지는 간략하게 알았으니 코드를 보면서 자세히 확인해봅시다.
🌱 iterable & Iterator
앞서 나왔듯, iterable한 객체는 iter가 가능(able)한 객체를 말합니다.
대표적인 iterable한 타입으로 list, dict, set, bytes, tuple, range, enumerate 등이 있습니다.
iterable객체는 공통적으로 `__iter__`라는 매직 메소드를 가지고 있습니다. 이 때문에 iterator() 함수를 인자를 씌우면 `next()`라는 함수로 순회할 수 있게되는 것입니다.
a = [1,2,3] # iterable
'__iter__' in dir(a) # True
b = a.__iter__() # iterator
'__next__' in dir(b) # True
- `next()` : 이터레이터에서 다음 값을 가져올 수 있는 함수이다.
- `iter()` : iterable 객체를 iterator로 바꿔주는 함수이다. 바뀐 이터레이터를 next()를 통해 값을 하나씩 가져올 수 있다.
만약, iterator가 iterable한 객체를 모두 순회했다면 `StopIteration`을 발생시킵니다.
다시 객체를 순회하고 싶으면 새로운 iterator 객체를 생성하면 됩니다.
iterable에 유용한 함수
이터레이블한 객체를 인수로 받아 사용하는 유용한 함수들이 있습니다.
대표적으로, `any` `all` `zip`이 있습니다.
all(iterable) | 인수의 모든 원소가 참인 경우 True, 그렇지 않으면 False 리턴 ex. 리스트 내 0의 여부를 확인할 때 사용 |
any(iterable) | 원소 중 하나라도 참이면 True, 모두 거짓일 때만 False |
zip(*iterable) | iterable한 객체를 인수로 동일한 개수로 이루어진 자료형을 묶어 반환 |
zip_longest(*iterable, fillvalue=None) | zip과 차이라면 원소의 갯수가 다를 경우 적은 쪽에 fillvalue를 채움 |
itertools.cycle(iterable) | 원소들을 계속 반복하고 싶을 때 사용 |
itertools.permutations(iterable, r=None) | 서로 다른 N개의 원소 중 r개를 중복적으로 선택하는 방법(순열) |
itertools.combinations(iterable, r=None) | 서로 다른 N개의 원소 중 서로 다른 r개를 중복 없이 선택하는 방법(조합) |
🌱 Generator
제너레이터(Generator)는 값을 하나씩 생성하는 파이썬의 강력한 도구입니다. 즉, iterator 객체를 만들 수 있는 함수를 말합니다.
그럼 제너레이터는 어떻게 표현할 수 있을까요? 다음과 같이 `yield`문이나 표현식을 사용해 만들 수 있습니다.
제너레이터는 큰 데이터 세트나 연속적인 데이터를 처리할 때 유용합니다.
큰 데이터를 메모리에 전체 저장하지 않고 필요한 만큼만 값을 생성해서 좀 더 효율적인 코드를 작성할 수 있기 때문입니다.
이로인해, 메모리 최적화에도 도움을 줄 수 있습니다.
다음 코드를 봅시다.
첫번째 함수는 제너레이터를 활용해 하나씩 값을 추출헀고, 두번째 함수는 리스트에 모든 값을 저장한다음 반환하는 식으로 코드를 작성했습니다. 이 둘의 메모리 차이를 보면 제너레이터를 사용했을 때 메모리가 적을 것입니다. 이유는 전체 데이터를 저장하지 않았기 때문입니다.
출력해보면 gen2는 리스트에 있는 모든 데이터를 반환하는 반면 gen은 `next(gen)`으로 하나씩 반환할 것입니다.
def gen_fun(n):
for i in range(n):
yield i
def gen_fun2(n):
lst = []
for i in range(n):
lst.append(i)
return lst
gen = gen_fun(100)
gen2 = gen_fun2(100)
print(sys.getsizeof(gen), sys.getsizeof(gen2))
next(gen)
아래 코드는 표현식만 다르게 했을 뿐 같은 제너레이터를 사용하고 있습니다.
리스트를 사용하면 크기만큼 메모리에 공간을 할당하게 됩니다. 그러나 next 함수로 호출하게되면 값을 생성하고 해당 값만 메모리에 올라가기 때문에 메모리 절약을 할 수 있습니다.
import sys
generator = (i for i in range(1, 100)) # 제너레이터 표현식을 작성했을 경우
iterator = [i for i in range(1, 100)]
print(sys.getsizeof(generator), sys.getsizeof(iterator))
또다른 방법, 이터레이터 클래스
제너레이터를 통해 이터레이터를 만들 수 있었지만 이터레이터 클래스로 이터레이터를 만들 수 있습니다.
아래 코드를 보면 `self.position`이라는 상태 값을 갖는 것을 확인할 수 있습니다. 이는 다음 값을 생성하기 위해 정보를 저장하는 변수 입니다. 이터레이터를 통해 이터레이터를 생성할 수 있습니다. 그러나, 제너레이터와 같은 가독성 좋고 쉬운 방법이 있는데.. 저는 제너레이터를 사용할 것 입니다.
class MyIterator:
def __init__(self, data):
self.data = data # 데이터
self.position = 0 # 상태 값 (현재 상태의 추적을 위해)
def __iter__(self):
return self # 이터레이터 객체 생성
def __next__(self):
if self.position >= len(self.data): # 데이터 길이보다 큰 경우 예외 발생
raise StopIteration
result = self.data[self.position] # 결과
self.position += 1 # 다음 추가
return result
items = MyIterator([1,2,3])
next(items)
제너레이터 관련 기능들
yield from
제너레이터를 합성해서 사용할 때 쓰이는 명령어입니다. 연속으로 제너레이터를 사용했을 때 합성한 경우와 수동으로 포함한 경우를 비교했을 때 벤치마킹 결과 합성했을 때가 더 빠른 시간 결과를 볼 수 있습니다.
send()
제너레이터의 값을 재개(Resume)할 때 사용하는 메서드입니다.
yield 가 끝나고 `send()`를 호출하면 해당 값이 `yield`의 결과 값으로 제너레이터를 진행합니다.
def generator():
value = yield 1 # 1. 1을 반환 -> 값을 보냄
print(value) # 3. Hello 출력
yield 2
gen = generator()
next(gen) # 0. 시작
gen.send('Hello') # 2. Hello 값을 보냄
💡 `send`를 사용할 때, 먼저 `next()`로 제너레이터를 시작한 다음 `send()`를 사용해야 한다.
throw()
`throw(exception_type)`은 제너레이터 함수에 예외를 던질 때 사용합니다.
제너레이터 함수에 예외 구문을 내포하여 사용할 수 있습니다. 다음 마지막 코드를 보면 예외 구문이 yield를 감싸는 것으로 볼 수 있습니다. . 물론, 예외를 발생시킬 수 있는 기능이 있지만 가독성 면에서 다시 예외를 발생시키는데 준비하는 코드가 필요하며 함수 또한 깊어지기 때문에 좋지 않다고 합니다.
def my_generator():
yield 1
try:
yield 2
except MyError:
print('에러 발생')
else:
yield 3
yield 4
class MyError(Exception):
pass
it = my_generator()
print(next(it)) # 1을 내놓음
print(next(it)) # 2를 내놓음
print(it.throw(MyError('test error'))) # 에러 발생 4
☕️ 포스팅이 도움이 되었던 자료
- How to Use Generators and yield in Python - Real Python
- Effective Python
오늘도 저의 포스트를 읽어주셔서 감사합니다.
설명이 부족하거나 이해하기 어렵거나 잘못된 부분이 있으면 부담없이 댓글로 남겨주시면 감사하겠습니다.
'Programming > Python' 카테고리의 다른 글
깊은 복사와 얕은 복사 (0) | 2024.04.08 |
---|---|
Garbage Collector 동작 방식 (0) | 2024.02.07 |
파이썬에서 멀티 스레딩을 구현하지 못하는 이유 (0) | 2024.01.31 |
파이썬 패키지와 모듈 (0) | 2024.01.02 |
파이썬 함수 Deep-Dive (0) | 2023.11.22 |