[Python] Generator
yield
라는 키워드가 사용되는 것을 generator라고 부른다. 아래 예시 코드를 통해 이해할 수 있다.
def frange(start, stop, increment): x = start while x < stop: yield x x += increment for n in frange(0, 4, 0.5): print(n , end=' ')
0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 |
위 코드는 특정 시작 값부터 종료 값까지 일정 증가값 간격으로 출력하는 코드다. 기본적으로 python에선 range() 함수를 통해서 사용 가능하지만, 소수점에 대해선 지원하지 않는다. frange
는 소수점을 지원하는 함수다.
yield x
에 도달하면 x의 값을 return하지만 함수는 종료되지 않고 계속해서 iteration을 수행하게 된다. 결국 while
조건에 부합하지 않을 때 함수가 종료된다. yield
는 함수 수행이 종료됐을 때 불필요하게 수행되는 것을 막기 위해 사용된다고 한다. 아래 예시 코드를 보면 더 이해가 쉽다.
def countdown(n): print('Starting to count from',n) while n > 0: yield n n -= 1 print('Done!') c = countdown(3) c
<generator object countdown at 0x00000223E79CF4C8> |
위 코드를 수행했을 때 Starting to count from
가 출력되지 않는다. 왜냐하면 이는 함수가 아니라 yield
가 들어가는 순간 generator
가 된다, 즉 iterator처럼 동작하게 된다. 실제 main 문과 함수 구문이 서로 번갈아가면서 수행되게 되기 때문이다.
next(c) # Starting to count from 3 next(c) # 2 next(c) # 1 next(c) # Done!
3 2 1 Done! |
위 결과를 보면 이해가 가기 더 쉽다. 처음 countdown(3)이 호출되면 함수가 수행되는게 아니라 단순하게 generator 객체를 return하는 것을 의미한다.
실제 수행은 next()
가 호출 돼야지 수행된다.
Advanced Generator
Iterator와 generator를 활용한 코드를 소개한다.
class Node: def __init__(self, value): self._value = value self._children = [] def __repr__(self): return 'Node({!r})'.format(self._value) def add_child(self, node): self._children.append(node) def __iter__(self): return iter(self._children) def depth_first(self): yield self for c in self: yield from c.depth_first() if __name__ == '__main__': root = Node(0) child1 = Node(1) child2 = Node(2) root.add_child(child1) root.add_child(child2) child1.add_child(Node(3)) child1.add_child(Node(4)) child2.add_child(Node(5)) for ch in root.depth_first(): print(ch, end=' ')
Node(0) Node(1) Node(3) Node(4) Node(2) Node(5) |
위 코드는 tree 구조처럼 생성되는데, main 함수에서 for 문을 다시 기술해보면 다음과 같이 된다.
# Previous for ch in root.depth_first(): print(ch, end=' ') # Alternative g = root.depth_first() while True: ch = next(g)
처음은 root의 generator 함수인 depth_first()
를 호출하게 되는데, 실제로 next()
가 호출 될 때 수행이 된다. depth_first()
의 yield까지 수행되고 print(ch, end=' ')
를 수행하면서 Node(0)
을 출력하게 된다.
그리고 다음 next()
가 call이 되면서 depth_first()
내부에 for문을 수행하게 된다. 해당 for문의 self는 본인의 iter()
를 의미하게 되고 __iter__
special function을 수행한다. 내부엔 본인의 children의 iter를 호출하게 된다. 참고로 for – in 문의 뒤에 선언되는 것은 내부 __iter__
를 호출한다. 코드를 풀어서 써보자면 아래처럼 될 것이다.
# Previous for ch in root.depth_first(): print(ch, end=' ') # Alternative g = root.depth_first() while True: ch = next(g) print(ch, end=' ') it = iter(self) while True: c = next(it) print(c, end=' ')
Reverse Iteration
class Countdown: def __init__(self, start): self.start = start def __iter__(self): n = self.start while n > 0: yield n n -= 1 def __reversed__(self): n = 1 while n <= self.start: yield n n += 1 c = Countdown(5) print("Forward:") for x in c: print(x, end=' ') print('\n') print("Reverse:") for x in reversed(c): print(x, end=' ')
Forward: 5 4 3 2 1 Reverse: 1 2 3 4 5 |
Python에선 reverse iteration을 __reversed__
special function을 구현해서 사용 가능하다.
Iterator 일부 얻기
def count(n): while True: yield n n += 1 c = count(0) c[5:10]
TypeError Traceback (most recent call last) <ipython-input-80-0e5ab4786dd3> in 5 6 c = count(0) —-> 7 c[5:10] TypeError: ‘generator’ object is not subscriptable |
위 코드는 Python에서 generator를 통해서 생성한 list를 접근을 하려고 하는 코드다. 그러나 실행하면 error를 발생하는데 이는 generator를 통해서 생성하기 위해선 next()
를 수행해야지 생성이 되기 때문이다. 이를 수행시킬 수 있도록 돕는 tool이 있다.
Definition
islice([Input], [Start Index], [End Index])
Example
import itertools for x in itertools.islice(c, 10, 20): print(x)
10 11 12 13 14 15 16 17 18 19 |
itertools에는 islice (iterator slice)라는 method가 있는데 이를 통해서 특정 부분을 쉽게 참조 할 수 있다.