Generator, Iterator, Iterable

2016-12-16 00:00:00
python generator iterator iterable

Python은 함수형 언어라 명시적으로 데이터 타입을 선언하지 않아서 데이터를 다룰 때는 매우 강력할 수 있지만 깔끔한 코드를 만들기 위해서는 잘 알고 쓰는 것이 좋습니다.

먼저 포함 관계를 이렇게 정리 할 수 있습니다.

$$ Generator \subset Iterator \subset Iterable $$

다시 말해, generator는 iterator가 될 수 있으나 반드시 iterator를 generator라고 할 수는 없으며 마찬가지로 iterable한 객체가 반드시 iterator는 것은 아닙니다.

Iterable

iterable은 형용사이지만 Python에서는 한번에 하나씩 return이 되는 하나의 객체를 일컫습니다.

__iter____getitem__ 메소드로 정의된 클래스는 모두 iterable 객체라 할 수 있습니다.

tuple, list, set, dict을 포함하는 컬렉션 객체는 물론이고 str같은 스트림 객체 역시 포함됩니다.

Iterator

Python에서 iterator는 next로 순회할 수 있는 객체를 말합니다.

여기서 컨테이너는 포괄적으로 데이터를 모아놓은 어떤 것이라고 생각해도 상관 없습니다.

데이터를 어떻게 순회하는지에 대한 개념이 iterator라고 할 수 있습니다.

기본적으로 iterator에는 다음 오브젝트를 가리키는 개념만 있으면 됩니다. 그렇기에 주로 tuple, list, set, dict같은 빌트인 컨테이너에 내부에는 __iter____next__(python 3)같은 메소드가 미리 정해져 있습니다.

Generator

generator는 iterator를 리턴하는 함수입니다. generator로 iterator를 만든다고 생각 하셔도 무방합니다.

주로 yield를 사용하여 함수를 리턴하게 되면 새로운 클래스에서 __iter__ 메서드를 쉽게 구현할 수 있습니다.

Example

next(iterator)

# help(next)
x = [ 1, 2, 3 ]
print(next(iter(x)))    # 1
print(next(x))          # Error!

x는 리스트이며 iterable 한 객체입니다. 하지만 next는 엄밀하게 iterator가 주어져야 하며 iterable 객체는 iterator가 될 수 없으므로 에러가 발생합니다.

for _ in iterable

x = [ 1, 2, 3 ] 
for i in x:
    print(x)            # 1 2 3
for i in iter(x):
    print(x)            # 1 2 3

for를 이용하여 순회하는 객체는 iterable한 객체입니다. iterator는 iterable 객체가 될 수 있으므로 실제로 내부적으로 iter(x)를 iterable 객체로 바꾸어 줍니다.

Set, Dictionary iterate

a = { 2, 8, 5, 7, 1, 4, 4 }     # set(object)
for i in a:
    print(i)                    # 1 2 4 5 7 8

d = {                           # dict(object)
    'd1':'1',
    'd2':'2',
    'd3':'3',
    'd4':'4'
}
for i in d:
    print(i)                    # random) d1 d2 d3 d4

iterator는 어떻게 순회할 지 정하기 나름이라 위와 같이 항상 앞에서 부터 순회한다고 할 수 없습니다. 특히 set에 대한 iterator는 중복된 원소를 제거할 뿐 아니라 순서도 반드시 일치하지가 않습니다. enumerate를 써도 앞에서부터 순서대로 짝지어주지 않으며 심지어 dict에서는 매번 실행할 때마다 순서가 바뀝니다.

Override

그렇다면 원하는 방향으로 순회하게 만들기 위해서는 직접 클래스를 만들어 __iter____next__를 오버라이딩 하는 방법이 있습니다.

# help(list) -> list([iterable])

class MyList(list):

    def __iter__(self):
        pass

    def __next__(self):
        pass

mylist = MyList([1,2,3])
for i in mylist:
    print(i)    # non-iterator error

help(list)에서 리스트는 iterable 객체로 초기화 할 수 있는 클래스임을 알 수 있습니다.

그래서 리스트를 상속받은 MyList 클래스를 새로 만들어 iterator를 초기화 시켜서 실제로 for 문을 통해 iterator가 NoneType이 되어 에러가 발생합니다.

Generator, for generate iterator

class MyRange:

    def __init__(self, a, b, c, d):
        self.a = a
        self.b = b
        self.c = c
        self.d = d

    def __iter__(self):
        yield self.b
        yield self.d
        yield self.a
        yield self.c
        yield self.b

myrange = MyRange(2, 3, 4, 5)
for i in myrange:
    print(i)    # 3 5 2 4 3

이번엔 임의로 iterable 객체를 만들어 본 것입니다. __init__메서드만 있다면 이 객체는 iterable 객체가 아니며 iterator도 없습니다. yield를 이용한 generator로 __iter__ 메서드를 만듭니다. 이렇게 기술한 순서대로 결과가 출력됨을 알 수 있습니다.

다시 말해 iterator 자리에 generator가 조립되어 들어간다라고 생각하셔도 됩니다. 그러므로 iterator를 만들기 위해 __iter__ 내부에 다음과 같이 iterable 객체를 분리해서 집어넣는 경우가 많습니다.

# inside class
def __iter__(self):
    for i in iterable:
        yield i

Exception next function

class MyStr(str):

    def __init__(self, string):
        super(MyList, self).__init__()
        self.string = string
        self.c = 0

    def __iter__(self):
        return self             # yield self

    def __next__(self):
        try:
            ret = self.string[self.c]
            self.c -= 3
            self.c %= 4
            return ret
        except IndexError:
            self.c = 0
            raise StopIteration

m = MyStr('golf')
for i in m:                     # golf (yield self)
    print(i)                    # g o l f g o l f ... (return self)

이번에는 str 스트림 객체 역시 iterable한 객체이므로 이를 상속받아 원하는 방향으로 순회하기 위한 예제입니다.

for 루프 에서 iterator는 단 한번만 호출됩니다. return self라고 하면 이후에 에러가 발생할 때까지 next를 반복합니다. 그렇기에 next메서드에서는 예외 처리를 해주어야합니다.

반면에 yield self라고 하면 generator로 생성된 iterator는 for루프를 통해 __iter__ 메서드를 한번만 호출했기 때문에 메서드가 끝나지 않고 여기서 정지하게 됩니다. next 메서드를 명시적으로 호출해 주지 않는 이상 자기 자신의 객체를 yield 갯수 만큼 호출할 뿐입니다. 이는 서브루틴과 코루틴의 차이라고도 할 수 있습니다.

yield from Reading

yield from gfor i in g: yield v와 같다고 생각하시면 됩니다. 중첩된 generator를 전달하게 되면 이를 iterable한 객체로 만들어 다시 전달하게 되는데 이 개념을 그대로 generator 만을 전달 할 수 있게 바꾸어 놓은 것입니다.

def subgen():
    for i in range(4):
        yield i

def gen(g):
    for i in g:
        yield i

def yf_gen(g):
    yield from g

for i in gen(subgen()):
    print(i)                    # 0 1 2 3

for i in yf_gen(subgen()):
    print(i)                    # 0 1 2 3

Sending data

지금까지 generator를 이용해서 iterator를 만드는 과정은 주로 iterable 객체를 for문을 통해 나누어서 iterator를 구성합니다. iterable 객체가 꼭 있어야 합니다.

여지껏 iterable 객체를 읽는 과정이었다면 이번엔 반대로 generator에 값을 전달하는 예제입니다. 소켓 등으로 데이터를 전송할 경우 등의 비동기 통신이 가능하게 됩니다. generator.send(value)로 전송 가능하며 먼저 None을 전달해야 합니다.

def gen():
    while True:
        recv = yield
        print(recv)             # 0 1 2 3

t = gen()
t.send(None)
for i in range(4):
    t.send(i)

yield from Sending

generator에 값을 전달하는 것 역시 중첩된 generator로 나타낼 수 있으며 간단하게 yield from문으로 generator만 전달 할 수 있습니다.

def subGen():
    while True:
        recv = yield
        print(recv)             # 0 1 2 4

def gen(g):
    g.send(None)
    while True:
        try:
            recv = yield
            g.send(recv)
        except StopIteration:
            pass

def yf_gen(g):
    yield from g

t = yf_gen(subGen())
t.send(None)
for i in [0, 1, 2, 'spam', 4]:
    if type(i) != int:
        pass
    else:
        t.send(i)

yield from 문은 generator를 일일이 iterator로 나눌 필요 없이 바로 호출 가능하기 때문에 주로 라이브러리를 이용할 때 유용합니다.

Reference

Concepts

Generator vs Iterator

yield from syntax