Python - Generator & Iterator

Nina·2020년 9월 21일
0

Python

목록 보기
4/6
post-thumbnail

1. Generator

(1) Lazy Evaluation


Generator는 lazy evaluation을 위해 사용될 수 있다. 먼저 다음의 예시를 보자.

def billion_numbers():
    nums = []
    i = 1
    while i <= 1000000000:
        nums.append(i)
        i += 1
    return nums

위 함수를 실행하면, 1억개의 element를 가진 list가 생성될 것이고, 컴퓨터 메모리에 저장될 것이다. 값이 실제로 쓰이기 전부터 모든 값이 생성 / 저장되기 때문에 비효율적이다. Generator를 사용하면 값이 필요할 때 생성함으로써 실행을 지연시키고, 이를 통해 효율성을 제고할 수 있다.

(2) Generator

Generator는 list와 유사한 구조를 return하기는 하지만, 다음과 같은 차이점이 있다.

🐍 List는 생성됨과 동시에 모든 element를 저장한다. Generator는 element가 필요할 때 생성한다.
🐍 List는 계속해서 반복(iterate)할 수 있지만, generator는 한 번만 가능하다.
🐍 List는 인덱스를 통해 element를 가져올 수 있지만, generator는 불가능하다.

Generator는 값을 메모리에 저장하지 않는다. 직전에 산출했던 값을 기억하고, 다음에 실행될 때 그 다음 값을 산출한다.

def hundred_numbers_with_generator():
    i = 0
    while i < 100:
        yield i
        i += 1

print('generator: ', hundred_numbers_with_generator())
print('generator:', [x * 2 for x in hundred_numbers_with_generator()])


hundred_numbers_with_generator()의 결과는 리스트가 아니라 generator object임을 확인할 수 있다.

(3) next

next()는 빌트인 함수로, '다음' 결과 값을 산출한다(중의적인 것 같아서.... next() is a built-in function, which returns the next value)

위 hundred_numbers_with_generator()에 이어서 코드를 작성하였다.

g = hundred_numbers_with_generator()
print(next(g))
print(next(g))
print(next(g))
print(list(g))


Generator는 마지막으로 산출했던 값을 기억하고 있다가, next로 호출했을 때 그 다음 값을 산출한다.

(4) Generator Class

아래의 클래스는 위 generator와 똑같은 결과를 갖는다.

class first_hundred_Generator:
    def __init__(self):
        self.number = 1
    
    def __next__(self): #iterator
        if self.number <= 100:
            current = self.number
            self.number += 1
            return current #returns 0
        else:
            raise StopIteration() #when reaching the end of generator, throw an error

my_gen = first_hundred_Generator()
print(my_gen.number) #1
my_gen.__next__()
print(my_gen.number) #2
print(next(my_gen)) #2
print(next(my_gen)) #3
print(next(my_gen)) #4

(5) Generator Expression

numbers_list_comprehension = [x for x in [6,7,8,9,10]]
numbers_gen_comprehension = (x for x in [6,7,8,9,10])

print(next(numbers_gen_comprehension)) #6

대괄호는 list comprehension이고 소괄호는 generator Expression이다. 소괄호라고 tuple comprehension일 것이라고 생각하지 말자.

2. Iterator & Iterable

Iterator: used to get the next value(step by step).
Iterable: used to go over all the values of the iterator

(1) Iterator

Iterator는 값을 순차적으로 꺼내올 수 있는 객체이다. Generator와 Iterator 값을 순차적으로 반환한다는 데에 공통점이 있지만, 다음과 같은 차이점이 있다.

🐍 Generator는 함수를 사용해 생성하지만 iterator는 iter()과 next() 함수를 사용해서 생성한다.
🐍 Generator에서는 yield 키워드를 사용하지만, iterator에서는 사용하지 않는다.

이 외에도 여러가지 차이점이 있지만, 쉽게 생각하면 generator는 iterator를 생성하는 함수라고 볼 수 있다. Generator를 사용하면 더 빠르고 컴팩트하게 코드를 작성할 수 있고, iterator를 사용하면 memory-efficient하다는 장점이 있다.

class FirstFiveIterator:
    def __init__(self):
        self.numbers = [1,2,3,4,5]
        self.i = 0
    def __next__(self):
        if self.i < len(self.numbers):
            current = self.numbers[self.i]
            self.i += 1
            return current
        else:
            raise StopIteration()

my_iter = FirstFiveIterator()
print(next(my_iter)) #1
print(next(my_iter)) #2
print(next(my_iter)) #3
print(next(my_iter)) #4
print(next(my_iter)) #5
print(next(my_iter)) #error stopIteration

(2) Iterable

Iterable은 '__iter__' method를 가지고 있는 object이다.

class FirstHundredIterable:
	def __iter__(self): 
    	#object에 __iter__ method를 사용하면, iterable이 되어 iterator을 return
	return FirstHundredGenerator()
    
print(sum(FirstHundredIterable()))

for i in FirstHundredIterable():
    print(i)


(캡쳐는 못했지만 100까지 출력되었다)

위 iterator를 사용하는 대신 generator에 __iter__ method를 추가함으로써 똑같은 결과를 출력할 수 있다.

class FirstHundredGenerator:
    def __init__(self):
        self.number = 1
    
    def __next__(self): #iterator
        if self.number <= 100:
            current = self.number
            self.number += 1
            return current #returns 0
        else:
            raise StopIteration() #reach the end of generator
    
    def __iter__(self):
        return self

print(sum(FirstHundredGenerator()))

for i in FirstHundredGenerator():
    print(i)


(또 짤렸지만 100까지 출력되었다. 진짜로.)

(3) __len__ & __getitem__

__len__과 __getitem__ method를 사용하여서도 object를 iterable로 만들 수 있다.

class AnotherIterable:
    def __init__(self):
        self.cars = ['Fiesta', 'Focus']

    def __len__(self):
        return len(self.cars)
    
    def __getitem__(self, i):
        return self.cars[i]

for car in AnotherIterable():
    print(car)

3. Examples

(1) Problem 1 - Iterator

🐍 Problem
딕셔너리도 반복가능한 객체라서 앞서본 리스트와 같이 __iter__함수와 __next__함수를 사용할 수 있고 파이썬 기본함수인 iter, next 또한 사용할 수 있습니다. 다음의 간단한 키를 출력하는 딕셔너리에 대한 for 문을 while문으로 구현해 보세요.

D = {'a':1, 'b':2, 'c':3}
for key in D.keys():
    print(key)

🐍 Answer

I = iter(D.keys())
while True:
    try:
        X = next(I)
    except StopIteration:
        break
    print(X)

(2) Problem 2 - Generator

🐍 Problem
다음 코드를 분석

import time

L = [1,2,3]

def print_iter(iter):
    for element in iter:
        print(element)

def lazy_return(num):
    print("sleep 1s")
    time.sleep(1)
    return num

print("comprehension_list=")
comprehension_list = [ lazy_return(i) for i in L ]
print_iter(comprehension_list)

print("generator_exp=")
generator_exp = ( lazy_return(i) for i in L )
print_iter(generator_exp)

🐍 Answer

#코드 실행 결과

comprehension_list=
sleep 1s 
#1초
sleep 1s
#1초
sleep 1s
#1초
1
2
3
generator_exp=
sleep 1s
#1초
1
sleep 1s
#1초
2
sleep 1s
#1초
3

List comprehension의 경우 sleep 1s 출력과 time.sleep이 먼저 실행되고 그 후 L에 대한 루프가 돌았다. 즉, lazy-return 함수가 먼저 반복되면서 실행되었고, lazy-return이 종료된 후에 print-iter 함수가 실행되었다. 한편 generator expression의 경우 lazy-return이 실행됨과 동시에 print_iter도 함께 반복 실행되었다. List comprehension은 lazy evaluation을 하지 않기 때문에 lazy-return이 멈추지 않고 반복되지만, generator expression은 lazy evaluation을 하기 때문에 함수가 실행되기 전까지 다음 반복을 실행하지 않는다.

profile
https://dev.to/ninahwang

0개의 댓글