오늘은 위코드 2주차 3일째! iterator
와 generators
에 대해 정리해보려한다.
list
, set
, dictionary
... 와 같은 객체들은 for문
을 써서 데이터를 하나씩 처리할 수 있는데, 이렇게 데이터를 하나하나 처리할 수 있는 반복 가능한 객체들을 iterable object
라고 한다.
그리고 iterable object
임을 확인할 수 있는 방법은 dir
로 호출하여 __iter__
함수가 있는지 보면 된다.
a = [1, 2, 3] # iterable 객체
print(dir(a))
>['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__',
'__gt__','__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__',
'__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__',
'__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop',
'remove', 'reverse', 'sort']
3번째 줄 마지막에 __iter__
함수가 있음을 확인할 수 있고, 이를 직접 사용하여 출력해보면 iterator
객체임을 확인할 수 있다.
a = [1, 2, 3]
print(a.__iter__()) # = print(iter(a))
> <list_iterator object at 0x1030021d0>
a
를 튜플이나 세트, 딕셔너리 형태로 바꾸게 되면 list
부분이 tuple
, set
, dict_key
의 형태로 바뀌어 출력된다.b = (1, 2, 3)
c = {1, 2, 3)
d = {1:"one", 2:"two", 3:"three"}
print(b.__iter__())
print(c.__iter__())
print(d.__iter__())
> <tuple_iterator object at 0x1027b0190>
<set_iterator object at 0x109bb4280>
<dict_keyiterator object at 0x10e6e1710>
다시 본론으로 와서 iterator_a
를 dir
로 찍어보게되면 __next__
라는 함수를 발견할 수 있다.
이 함수는 이름에서 유추할 수 있듯이 다음 요소를 하나씩 꺼내오는 함수이다.
현재 a
안에는 3개의 값이 들어있으므로 이 __next__
함수를 3번 호출하게 되면 1, 2, 3이 출력된다.
여기서 한 번 더 호출을 하게 되면 StopIteration
이 발생하게 된다. 이는 다음 존재하는 값이 없기 문에 말 그대로 iter
에 대한 명령을 멈춘다는 뜻이다. iter 멈춰!
print(iterator_a.__next__()) # = print(next(iterator_a))
print(iterator_a.__next__())
print(iterator_a.__next__())
# print(iterator_a.__next__())
>1
2
3
#StopIteration
이제 while
문을 이용하여 a
를 제곱한 코드를 구현해보자.
a = [1, 2, 3]
iterator_a = iter(a)
while True:
try:
next_a = next(iterator_a)
except StopIteration:
break
print(next_a ** 2)
> 1
4
9
이번엔 리스트가 아닌 딕셔너리에 대한 for문
을 iter
를 사용하여 while문
으로 바꿔보자.
D = {'a':1, 'b':2, 'c':3}
#for key in D.keys():
# print(key)
# > a
# b
# c
while True:
try:
iterator_D = iter(D)
next_D = next(iterator_D)
except StopIteration:
break
print(next_D)
> a
b
c
보통 파이썬 내에서 함수는 그 값을 반환하고 종료한다. 하지만 generator
함수는 값을 반환하기는 하지만 산출 (yeild) 한다는 차이점이 있다. 이 generator
는 iterator
를 생성해주는 함수라고 볼 수 있다.
이 generator
는 iterable
한 순서가 지정되기에 generator
는 iterator
라고 볼 수 있다.
또한 이 generator
를 dir
함수로 찍어보게 되면 iterator
와는 다르게 __iter__
와 __next__
함수가 둘 다 들어있음을 확인할 수 있다. 때문에 __iter__
를 먼저 호출하지 않아도 __next__
함수를 바로 호출할 수 있다.
예시를 보자.
def generator_number():
for i in range(4):
yield i
print(generator_number())
> <generator object generator_squares at 0x10f5d69d0>
gen = generator_number()
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# print(next(gen)) # StopIteration
print(type(gen)) # generator
iterator
와 마찬가지로 정해진 값을 넘어서면 StopIteration
이 발생한다는 것을 확인할 수 있다.
여기서 갑자기 궁금한 것이 생겼다. generator_number()
함수를 gen
에 따로 담지 않고 그냥 돌리게 되면 어떻게 될까? 그저 길이가 길어져서 이렇게 하는 걸까?
print(next(generator_number())) # 0
print(next(generator_number())) # 0
print(next(generator_number())) # 0
print(type(generator_number())) # generator
몇 번을 넣어봐도 값은 0
. 즉 첫 값이 나온다. 분명 type을 찍어봤을 땐 둘 다 generator
인데..
왜 그런가 알아보니 아래와 같은 방법으로 했을 경우엔 세 개의 generator_number()
가 각각 다른 generator
로 인식이 된다는 것이다. 이는 위에서 했던 iterator
에서도 마찬가지였다. 알다가도 모를 파이썬 세계..
iterable_a = [1, 2, 3]
print(next(iter(iterabl_a))) # 1
print(next(iter(iterabl_a))) # 1
print(next(iter(iterabl_a))) # 1
print(type(iter(iterabl_a))) # list_iterator
그리고 이 generator
의 표현식은 Lazy Evaluation
을 위해서 사용될 수 있다.
여기서 Lazy Evaluation
란 말 그대로 실행을 지연시킨다는 의미인데, 어떤 값이 실제로 쓰일 때까지 그 값의 계산을 뒤로 미루는 동작 방식이다.
Lazy Evaluation
를 이해하기에 좋은 블로그이다.
https://itholic.github.io/python-lazy-evaluation/
https://bluese05.tistory.com/56
이 블로그에서 사용한 예시를 하나 가져와봤다.
아래 함수는 1초간 print
를 수행한 후 x
값을 return
하는 함수이다. 그 때 list
와 generator
를 생성했을 때 차이를 볼 것이다. (generator expression
은 list comprehension
과 똑같이 사용할 수 있는데 이 때 차이점은 [ ]
대괄호로 묶는 것이 아니라 ( )
소괄호로 묶는다는 점이다.)
def isay_func(x):
print "I say.."
time.sleep(1)
return x
#list생성
isay_list = [isay_func(x) for x in range(3)]
for i in isay_list:
print(i)
>I say...
I say...
I say...
0
1
2
#generator 생성
isay_generator = (isay_func(x) for x in range(3))
for i in isay_generator:
print(i)
>I say...
0
I say...
1
I say...
2
결과를 보면 list
의 경우에는 I say...
가 1초 간격으로 출력이 된 후0,1,2
값이 출력이 됐다.
결과 값에서도 다르게 보이는 generator
의 경우엔 I say...
와 0
을 출력 후 1초 간격으로 I say... 1
, I say... 2
를 출력함을 볼 수 있다.
이는 코드가 더 길어질 경우 시간과 메모리를 절약할 수 있다.
내용은 이해했지만 아직 실제로 나의 공부 과정에 적용하기엔 어려움이 있다.
좀 더 공부하고, 더 많은 예시를 보면서 나에게도 적용시킬 수 있도록 노력해야겠다.