Iterable

teal·2023년 8월 18일
0

Python

목록 보기
7/8

iterable

파이썬에서 iterable은 사용안하는 곳이 없을 정도로 중요하고 많이 사용되는 개념이다.

평소에 사용하는 list, dict, set 등의 객체를 보자. 우리는 이 객체를 사용할때 for문에 넣어서 쓰거나 map의 인자로 넣어서 사용하기도 한다.

map 내장 함수를 한번 보자

class map(Iterator[_S], Generic[_S]):
    @overload
    def __init__(self, __func: Callable[[_T1], _S], __iter1: Iterable[_T1]) -> None: ...

위와같이 map(함수, 반복 가능한 객체)를 넣게 된다. 여기서 반복 가능한 객체를 보면 Iterable으로 나타난 것을 볼수 있다. 그러고보면 이 Iterable 객체들은 반복할 때 다음 원소를 어떤 방법으로 찾아가게 되는걸까?

파이썬에서 iterable은 반복 가능한 객체로 iter 함수를 메소드로 가지고 있어야하고 해당 함수는 iterator를 반환해야한다.
참조

그러면 또 iter는 왜 필요한거지? iterator는 또 뭔가? 라는 생각이든다.

한번 자주쓰는 range와 비슷한 클래스를 만들어보자.

class CharacterRange:
    def __init__(self, start:str, end:str):
        self._start : int = ord(start)
        self._end : int = ord(end)

    def __iter__(self):
        print("call __iter__")
        return self
    
    def __next__(self) -> str:
        print("call __next__")
        if self._start != self._end:
            self._start += 1
            return chr(self._start - 1)
        else:
            raise StopIteration

위의 클래스로 만든 객체는 start, end를 받아서 start부터 시작하여 end전까지의 문자를 반환하는 기능을 가지고있다. iter를 가지고 있고 iternext가 가능한 객체(자신)을 반환하기에 iterable한 객체인다.

한번 출력해보자

c_range = CharacterRange('a', 'e')
for c in c_range:
    print(c)
call __iter__
call __next__
a
call __next__
b
call __next__
c
call __next__
d
call __next__

위 코드를 보면 먼저 for문에서 iter함수를 통해 iterator를 반환받고 해당 객체로 하나씩 next 함수를 통해 각 원소에 접하는것을 확인할 수 있다.

그런데 위 구현대로라면 좀 아쉬운점이 생긴다. c_range 객체를 활용해서 한번 더 반복돌리려고 하면 start가 end와 같은 값이기에 곧바로 StopIteration으로 멈추게된다. 이를 range처럼 재활용 하기 위해서는 iterator를 분리해야한다.

class CharacterRange:
    def __init__(self, start:str, end:str):
        self._start : str = start
        self._end : str = end

    def __iter__(self):
        print("call __iter__")
        return self.CharacterIterator(self._start, self._end)
    

    class CharacterIterator:
        def __init__(self, start:str, end:str):
            self._start : int = ord(start)
            self._end : int = ord(end)
        def __next__(self) -> str:
            if self._start != self._end:
                self._start += 1
                return chr(self._start - 1)
            else:
                raise StopIteration

위와같이 iterator를 분리시키고

c_range = CharacterRange('a', 'e')
for c in c_range:
    print(c)

for c in c_range:
    print(c)

위와같이 두번 반복시켜도

call __iter__
a
b
c
d
call __iter__
a
b
c
d

우리가 원하는 결과를 얻을 수 있다. for 문에 들어갈때마다 iterator 객체가 새로 생성되어 해당 iterator로 반복을 돌기 때문이다.

그리고 파이썬 내장함수로 구현되어있는 iter, next 함수는 각각 해당 객체의 iter 함수와 next 함수를 동작시켜준다.

arr = [1, 2, 3]
print(next(arr))

그래서 위와 같이 코드를 동작시키면 list는 iterable 객체지만 iterator가 아니기에 type 에러가 발생한다. 이럴때는 iter 내장함수로 iterator 객체를 반환받아서 사용할 수 있다.

arr_iterator = iter([1, 2, 3])
print(next(arr_iterator))

generator

파이썬에서 iterator를 생성하기 위한 방법으론 generator라는 방법도 존재한다. generator의 특징은 해당 값이 필요하기 전엔 미리 연산을 하지 않는 지연 연산(Lazy Evaluation)을 사용한다는 것이다.

지연 연산이 뭘까? 우리가 평소에 개발할때 generator같이 쓰는 내장함수들이 존재한다. 대표적으로 filter와, map이 있다. 두가지 내장함수 통해 나온 이터러블 객체를 list로 캐스팅해서 사용할시 제너레이터의 이점이 사라지게 된다.

만약에 리스트에서 5와 10 사이에 있는 가장 앞에 있는 값 하나를 찾고 싶다고 하면 해당 리스트를 반복문으로 돌면서 찾으면 break로 나가는 방식도 있을것이고 filter를 사용한다면 filter(lambda x: 5 < x < 10, arr)와 같이 이터러블 객체를 만든 후 next를 통해 첫번째 값을 찾고 끝낼 수 있을 것이다.

간단한게 generator를 만들어보자. generator는 함수에서 return대신 yield로 반환하게 된다.

def split_and_square_chunks(arr: List[Any], chunk_size: int = 10) -> Generator:
    for i in range(0, len(arr), chunk_size):
        yield [i*i for i in arr[i:i + chunk_size]]

arr = list(range(20))
for chunk in split_and_square_chunks(arr, 5):
    print(chunk)
[0, 1, 4, 9, 16]
[25, 36, 49, 64, 81]
[100, 121, 144, 169, 196]
[225, 256, 289, 324, 361]

위 제너레이터는 리스트를 입력받아서 chunk_size만큼씩 쪼개고 해당 값들에 제곱을 하여 리턴한다. 위의 코드는 전체를 다 돌았지만 만약 중간에 필요한 조건이 모두 충족되면 다 돌 필요없이 반복문을 break를 통해 나가면 필요한 부분까지만 연산을 진행하게 된다.

제너레이터도 이터러블 객체인지 한번 보자

print(split_and_square_chunks(arr, 5))
print(iter(split_and_square_chunks(arr, 5)))
print(next(iter(split_and_square_chunks(arr, 5))))
<generator object split_and_square_chunks at 0x103b533c0>
<generator object split_and_square_chunks at 0x103b533c0>
[0, 1, 4, 9, 16]

위와같이 iter, next 메소드가 전부 구현되어 있는것을 볼 수 있다. 제너레이터는 이터러블 객체지만 맨위의 CharacterRange처럼 iter가 자기자신을 반환하고 있기 때문에 한번 다 끝까지 돌면 iterator를 분리시킨것과는 다르게 다시 재활용은 불가능한 특징을 가진다.

profile
고양이를 키우는 백엔드 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 18일

좋은 글 감사합니다. 자주 방문할게요 :)

답글 달기