Python generator

박상영·2020년 6월 1일
0

Iterable, Iterator 두가지의 의미를 먼저 알고가자.

iterable 은 number를 하나씩 차례로 반환 가능한 object 를 말한다.

iterable 의 예로는 sequence tpye(시퀀스) 인 list, str, tuple이 대표적이다.

for i in range(10):
    print(i)
0
1
.
.
9

위에 코드는 range()로 생성된 list가 iterable(반복가능) 하기 때문에 순차적으로 number를 불러서 사용이 가능하다.

하지만 non-sequence type(시퀀스가 아닌) 인 dictionary 나 file 도 iterable 하다고 볼수있다. dict 또한 for 문을 이용해 순차적으로 접근이 가능하기때문이다.

x = {"a" : 3, "b": 5, "c" : 7}
for key, val in x.items():
    print(key, val)
    
a 3   # 출력값
b 5
c 7

이런식으로 .items()를 사용하여 keys와 values를 각각 출력할수도있다.

또한 __iter__()__getitem__() 메소드로 정의된 class 도 모두 iterable 하다고 볼수있다. iterable은 for loop 말고도, zip(), map()과 같이 sequence한 특징을 필요로 하는 작업에 유용하게 사용되고, zip()이나 map() 함수의 경우는 iterable 을 argument 로 받는것으로 정의되어있다.

Iterator 는 next() 메소드로 데이터를 순차적으로 호출 가능한 object 이다. 만약에 next()로 다음 데이터를 볼러올수 없을 경우 StopIteration exception 을 발생시킨다.

iteralbe 한 object는 iterator 인가?
반드시 iteralbe이 iterator는 아니다.

iter_val = [1, 2, 3]
next(iter_val)
"TypeError: 'list' object is not an iterator"

list object는 iterator 가 아니라고 Error가 발생한다. 하지만 위에 설명했듯 list는 iterable 인데 왜 iterator가 아닐까?
iterable을 iterator로 반환시켜주면 된다.

iter_val = [1, 2, 3]
print(type(iter_val))
<class 'list'>  # iter_val 은 iterator 가 아닌 그냥 list type 이다.

real_iter_val = iter(iter_val) #iter()를 사용하여 iterator로 반환한다.
print(type(real_iter_val))
<class 'list_iterator'> # 이렇게 list는 iterator 가 된것을 볼수있다.

Generator 는 무엇인가.

generator 는 iterator 를 생성해 주는 function 이다. iterator 는 next() 메소드를 이용해 데이터를 순차적으로 접근이 가능한 object 이다.

generator는 일반적인 함수와 비슷하게 보이지만, 가장 큰 차이점은 yield 가 들어가는것이다.

def generator_squares():   
    for i in range(1, 30):
        yield i ** 2  #yield에 값을 산출한다.
        
print("gen object=", end='')
print(generator_squares())

gen object=<generator object generator_squares at 0x109d3c0b0>

generator_squares 함수는 generator object 인것이다.
yield는 제너레이터 함수에서 값을 반환할 때 사용하며 yield 호출후에 다시 next가 호출될때 까지 현재의 상태에서 머물고 있다가 next 함수가 호출되면 이전의 상태에 이어서 다음 연산을 수행한다.

gene = generator_squares()
print(gene.__next__())
print(gene.__next__())
#출력결과
1
4

__next__() 를 사용한 이유는 dir()로 함수 종류를 확인하면 알수있다.

print(dir(generator_squares()))

['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw'] 리스트 형식으로 많은 함수의 종류가 출력된다. 위에__iter____next__도 포함되어있는것을 볼수있다.
즉 generator_squares 함수는 이미 iterator이기 때문에 __iter__ 를 호출하지않고 __next__를 바로 호출할 수 있다는것이다.

이터레이터와 마찬가지로 for문이 끝나게 되면 StopIteration가 발생하게 된다.

send 함수

제너레이터 함수는 실행중에 send함수를 통해서 값을 전달할 수도있다.
send()는 "arg" 를 generator에 보낸다음, 다음 산출 값을 반환 하거나 StopIteration을 발생시킵니다.

def generator_send():
    received_value = 0 # 먼저 변수를 지정해줍니다.
    while True:   
        received_value = yield  #1
        print("received_value = ", end="")
        print(received_value)  #3
        yield received_value * 3 #2

#1. yield를 통해 send로 받은 value를 received_value에 할당한다.

#2. 1번에서 할당받은 값을 *3을 return 합니다.

#3. 2번에서 return한 값을 출력합니다.

gen = generator_send()
next(gen) #1
print(gen.send(2)) #2

next(gen)
print(gen.send(6))

#출력결과
received_value = 2
6
received_value = 6
18

이처럼 send()를 통해 2, 6을 보내고 yield 로 *3 한 값을 반환하여 값이 출력되었다.
next()를 사용하지않고 send()를 먼저 사용하면 어떻게될까?
TypeError: can't send non-None value to a just-started generator
"None 이 아닌 값을 방금 시작 한 generator로 보낼 수 없다."
실행 순서에 맞지않기때문에 Error가 발생합니다.
yield는 표현식(expression) 입니다.
1. yield표현식은 최초의 yield가 일어난 후에 전달됩니다.
2. send() 메소드는 yield로 값을 전달 한 후에, callernext()send() 를 호출해주기를 기다리고있는 generator에만 사용될수있다.

그러므로 먼저 caller인 next()를 사용하여 호출한 다음 send()로 값을 보내어 출력결과를 얻게됩니다.

generator expression(제너레이터 표현식)

generator expression 는 Lazy evaluation 을 위해 사용할 수 있습니다.
Lazy evaluation 은 어떤 값이 실제로 쓰일 때 까지 그 값의 계산을 뒤로 미루는 동작방식을 가지고 있습니다.

import time
L = [1, 2, 3]
def print_iter(iter):
    for elem in iter:
        print(elem)
        
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)

먼저 import 한 time은 time.sleep(sec) 라는 기능을 가지고있습니다.
이 기능은 sec에 몇 sec에 한번 출력을 실행할껀지 를 주는 기능입니다.

먼저 list comprehension에 list의 값을 저장한것을 볼수있다. 하지만 list를 만들기 전 이미 lazy_return 함수가 동작하여 "sleep 1s" 라는값을 제일 먼저 다 받아놓는다.

sleep 1s
sleep 1s
sleep 1s

그다음 print_iter함수에서 L 리스트값을 하나하나 전달하여 출력하게된다.

1
2
3

그다음 generator expression을 사용한 식을 보면

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

대괄호([]) 도아니고 중괄호({})도아닌 소괄호(())를 사용한것을 볼수있다.
이는 generator로 생성했을때는 값 호출시 Lazy Evaluation으로 동작한다.

위에서 List comprehension과 Lazy evaluation과의 차이는
List comprehension은 미리 출력될 값을 받아 출력했다면, Lazy Evaluation은 generator expression을 선언하기전에 값을 받지않는다.
그러므로 미리 출력되지않고, generator_exp = (lazy_return(i) for i in L) exp를 선언하게 되는 순간 lazy_return(1)이란 값을 순서대로 받은값을 출력하게된다.

lazy_return(1)
#출력결과
sleep 1s
1

lazy_return(2)
#출력결과
sleep 1s
2

lazy evaluation의 장점

이 장점은 위의 식에서도 들어나는데
<1>. 필요할때만 평가되기 때문에 메모리를 효율적으로 사용할 수 있다.

위에서 list comprehension은 미리 값을 받아 출력하는 반면 lazy evaluation은 값이 주어지기 전에는 아무 반응을 하지않고 있다가 딱 주어졌을때 값이 return 이 된다.

<2>. 무한 자료구조를 만들어 사용할 수 있다.

<3>. 실행 도중의 오류상태를 피할 수 있다.(컴파일 타임 체킹)

<4>. 컴파일러 최적화 가능

결국 코딩의 스킬을 결정하는건 그 코드가 보기에 좋은지도 있지만, 빅데이터 시대에서 자료를 만드는것이 중요하기때문에 무한한 자료구조 또한 만들수 있다는것이 최대 장점이라 생각한다.
또한 그 크기의 자료구조가 메모리를 효율적으로 사용하지않는다면, 대게 컴퓨터는 힘이 딸릴수도있다.

profile
backend

0개의 댓글