Python - Iterator

solee·2022년 2월 5일
0

Python

목록 보기
5/16

iterator"값을 순서대로 꺼낼 수 있는 객체" 를 말한다.
그럼 개발자 머리에 톡 떠오르는 게 있다. 값을 순서대로 꺼낸다? list 같은 거 아냐?

조금 다르다. 그것들은 정확히 표현하자면 iterable한 객체이다. 또 다시 표현하자면, __next__를 가진 객체가 바로 iterator이다.

패키지를 상대 경로로 import할 때 찾았던 __name__이 기억나는가? 이 또한 파이썬 내장 객체였다. 이렇게 앞뒤로 언더바 (혹은 언더스코어)_가 두 개씩 들어간 것들이 파이썬 내장 객체들이다.


그러면 정리해 볼까.

  1. iterator란 __next__함수를 가진 객체를 말한다.

  2. 또 동시에 iterator란 값을 순서대로 꺼낼 수 있는 객체다.

  3. 그러면 __next__함수가 무슨 일을 하는지 알겠는가?

  4. 그렇다! 바로 다음 값을 꺼내는 일이다!




Iterator 사용법

그러면 이 iterator라는 것은 어떻게 사용하면 될까? 먼저 그 사용에 대해 말해보자면, 간단하게 iter()__next__로만 나눠 보겠다. 전자는 iterator로 형변환하는 것이고 후자는 iterator의 기능을 이용해 다음 값을 꺼내는 것이다.
사용 예는 다음과 같다.

a = [1, 2, 3, 4]
a = iter(a)
print(a.__next__())

출력하면 1이 나온다.

print(a.__next__())
print(a.__next__())
print(a.__next__())

이렇게 출력하면, print()가 자동으로 개행하므로

1
2
3

이런 결과가 나올 것이다. 하품이 나올 만큼 간단하다. iter()로 형변환해 iterator가 가진 기능을 사용하는 것이다.


그런데 뭔가 이상하지 않는가?
왜 형변환은 iter()이고 다음 값을 꺼낼 때엔 __next__일까?
이것들은 바로 그 반대로도 사용할 수 있다.

b = [1, 2, 3, 4]
b = b.__iter__()
print(next(b))

오.
그렇다. __iter__iter()는 같고 next()__next__는 같다.
정확히는, ~~~()가 객체의 __~~~__를 호출하는 형태라고 할 수 있겠다.





그러면 리스트의 크기보다 더 많이 __next__를 사용하면 어떻게 될까?

에러가 발생하고 프로그램이 종료된다.

엥? 그럼 이걸 굳이 쓰는 이유가 뭐지? for같은 반복문이 있는데...
반대다! for문이 바로 이 iter()를 사용한다. 파이썬 docs에서 for문의 맨 첫 설명을 확인하면 다음과 같다.

iterable 객체들의 요소를 iterate한단다.




iter ( object [ , sentinel ] )

그러면 언제 iter()함수를 사용하는 걸까? 형변환을 할 때만 필요한 걸까? 이것을 읽어보자.

먼저 신경써야 할 것은 이것이다.

iter ( object [ , sentinel ] )

object는 알겠는데 sentinel이란 뭘까? 바로 '보초'다. 두 번째 인자는 iter()함수를 지켜보고 감시하다가 그 값이 sentinel, 두 번째 인자와 같은 값이 되면 iter()함수를 종료시킨다. 두 번째 인자는 optional해서 넣어도 되지만 아까의 예시 코드처럼 넣지 않아도 된다.

두 번째 인자는 보았으니, 첫 번째 인자를 읽어보면 특이한 것이 있다.

  • sentinel이 없을 때, object는 반드시 __iter__메서드를 지원하는 collection이어야 한다.

str, list, tuple, dict 등...

  • sentinel이 있을 때, object는 반드시 callable object여야 한다.
a = ["a","b","c","d"]

iter(a)
iter(a, "d")

어떻게 될 것 같은가? iter(a)는 아까 확인했다시피 아무 문제 없이 형변환 할 수 있다. 그러면 두 번째 iter(a, "d")는?

에러가 발생한다.

object는 callable 해야 한다.

그럼 list는 callable하지 않나? 무슨 소리야 이거?

여기서 또 하나가 튀어나온다. 바로 함수다. callable()이라는 형태로 쓰이며, 해당 함수나 객체가 호출 가능한지에 대한 T/F를 반환한다고 한다. 정확히는 해당 함수 또는 객체에 __call__메서드가 존재하는지를 확인한다고 한다.
그래서 이렇게 넣어 보면...

print(callable(a))

False가 출력된다. 즉, list는 callable하지 않으므로 iter(object, sentinel)에서 object에 사용할 수 없다.

일단 다른 방법을 시도해 봤다.

a = ["a","b","c","d"]

aa = iter(a)
b = iter(aa, "d")

list가 callable하지 않다면, 그 list를 iter()로 list_iterator로 만든 aa를 사용하면 어떨까???? 놀랍게도 똑같이 안 됐다.

list를 iter()하면 list_iterator가 되고, set을 하면 set_iterator가 된다. 그러니까 list도 list_iterator도 callable하지 않다는 거다.(마찬가지로 callable(aa)하면 False가 나온다)

또다시 의문.

iter(o, sentinel)에서 두 번째 인자 없이 o는 반드시 collection이어야 하고 list는 collection이니 이해하겠는데, list_iterator는 왜 불가능할까?

아래로는 속시원한 답은 아니다. 그냥 내가 찾고 생각해 본 결과니 부디 다르다면 누가 알려주면 좋겠다.



그럼 뭐가 callable할까?

검색해 봐도 callable에 대해서는 직접 생성한 클래스에 관해 설명하는 경우가 많았고, 스택 오버플로우에서도 내가 원하는 설명을 찾을 수가 없었다. 심지어 에러 중에 list가 callable하지 않다는 이름의 에러가 있었는데, 이건 list()처럼 사용했을 때, 즉 list를 함수처럼 사용했을 때 나타나는 에러였다. 호출할 수 있다는, call할 수 있다는 게 정확히 뭘까?

파이썬 docs다.

Note that classes are callable (calling a class returns a new instance); instances are callable if their class has a __call__() method.

이 설명에 따르면... list와 list_iterator는 __call__메서드를 가지고 있지 않다는 거다.

파이썬 콘솔에 help(iter)하면 이런 답도 나온다.

iter(...)
    iter(iterable) -> iterator
    iter(callable, sentinel) -> iterator
    
    Get an iterator from an object. In the first form, the argument must
    supply its own iterator, or be a sequence.
    In the second form, the callable is called until it returns the sen
tinel.

스택 오버플로우에 다들 복붙한 것처럼 비슷한 이야기를 하더니 이거였구나.(물론 help(iter)해보라는 글도 스택오버플로우에서 찾았다)


이런 예시도 찾았다.
count = 1
def nextcount():
    global count
    count *= 2
    return count

print(list(iter(nextcount, 16)))

callable()도 찍어 봤고, True가 나왔다. dir()로 찍어 보니,

['__annotations__', '__call__', '__class__', '__closure__', '__code__', 
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__get__', '__getattribute__', 
'__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', 
'__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', 
'__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', 
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
임의로 개행했다

__call__이 있다.


이런 예시

import itertools
for val in itertools.takewhile(l, lambda x: x!= 4):
    print(val) 

도 찾았는데, 람다를 사용해 결국은 함수(아직 람다를 제대로 공부한 건 아니지만, 람다는 함수로 만들 수 있다는 점에서...)에서 iter(o,sentinel)을 사용하는 모양이다.
list같은 경우 호출 가능하긴 하지만 객체 list를 반환하는 것이므로 __call__이 없다는 답변이 있었다.




번외: from functools import partial

자아, 허망한 결론(?)을 내 놓고도 아직 끝나지 않았다.

여기에 뭔가가 하나 더 남아 있기 때문이다! 초록색 블록을 읽어 보자. iter()의 유용한 두 번째 형태라고 한다.

binary database file에서 고정된 크기의 블록을 읽는 코드라고 한다.

from functools import partial
with open('mydata.db', 'rb') as f:
    for block in iter(partial(f.read, 64), b''):
        process_block(block)

엉덩이가 아프지만, 오기가 생겨 찾아보았다.


아직 파이썬의 파일에 읽고 쓰는 함수들을 배우지 않아서 그냥 깡으로 조각조각 찾아 조각모음을 해 보려고 한다.


첫 번째.

from functools import partial

functools에서 partial이라는 함수를 import하는군. 함수를 여러 버전으로 관리할 때에 사용한다고 한다. 인자를 각각의 조건에 따라 미리 채워주는 역할을 한다고나 할까? 여기를 참고했다.


with open('mydata.db', 'rb') as f:

open()함수는 파일을 열어주는 역할을 하는데, 첫 번째 인자가 파일 이름이고 두 번째 인자가 방식을 결정한다. r은 읽기 모드, b는 예상하겠지만 binary모드다.

with ... as를 이용해 파일이 끝나면 자동으로 종료되게 하고 f라는 객체를 사용한다.


for block in iter(partial(f.read, 64), b''):

이제 for문이다. 가장 안쪽부터 읽자면 f.read가 보인다. 객체 f를 읽어들인다.
partial(f.read, 64) 이 코드로 read()에 64라는 인자를 준다. 그러면 64개어치의 문자를 읽어 오는 것이다. 왜 바로 f.read(64)를 하지 않았을까?? 이유는 다음과 같다.

dir(partial) = ['__call__', '__class__', '__delattr__', ... ]

partial이 __call__를 가지고 있기 때문에, 그 앞에 있는 iter(o, sentinel)에 첫 번째 인자로 들어갈 수 있기 때문이다!

그러니 이 코드는, 읽어온 파일 객체의 64개 문자를 block이라는 객체에 집어넣어 for문을 돌린다.


process_block(block)

이건... 이건 못 찾았다. docs나 help()에서도...

def process_block(block):
    for record in block:
        process_record(record)

이런 설명까지는 찾았는데(스레드에서 이런 역할을 한다고 한다) 결국 이제 process_record가 뭐 하는 놈이냐도 찾아봤는데, aws와 Kinesis Client Library로 연결되고 정말 뭔지 모르겠기에 멈췄다. 아무튼... 읽어들인 값 block을 처리하는 내용이겠지!



마치며

아무튼.. 조금이나마 속이 시원하게 끝낼 수 있어서 기쁘다. 계속 공부해서 이 포스트를 더 알차게 수정하고 싶다. 그날까지 파이팅!!!

profile
DA DA DA

0개의 댓글