Story 05 Iterable 객체와 Iterator 객체

유동헌·2021년 9월 27일
0

열혈파이썬_중급

목록 보기
5/8

Iter 함수

하나 이상의 값을 저장하는 일과 저장된 값들을 하나씩 꺼내 보는 일은 단순하지만 중요한 일이다. 그리고 자주 필요한 일이기도 하다. 물론 우리는 다음과 같이 for loop를 기반으로 이러한 일을 진행할 수 있다.

ds = [1,2,3,4]

for i in ds:
    print(i, end = ' ')

# 
1 2 3 4

사실 위의 방법만으로도 원하는 일 대부분을 처리할 수 있다. 그러나 값을 꺼내는 방법이 조금 더 유연하다면 우리가 할 수 있는 일의 범위는 더 넓어질 것이다. 그래서 그 유연한 방법을 소개하고자 한다.

그럼 이와 관련해서 다음 예를 보자.

ds = [1,2,3,4]

ir = iter(ds) # iterator 객체를 얻는 방법

print(next(ir)) # iterator 객체를 통해 값을 하나 꺼내는 방법
print(next(ir))
print(next(ir))
print(next(ir))

#
1
2
3
4

위 예제에서는 iter라는 이름의 함수를 호출하면서 리스트를 전달했다. 그러면 iter 함수는 객체를 생성해서 반환하는데, 이는 리스트에서 값을 꺼내는 기능을 제공하는 객체이다.

Iterator 객체 <접근> → ds = [1,2,3,4] <함수에 전달> → iter() 함수

즉 위 예제에서는 리스트 ds를 전달하면서 iter 함수를 호출하였다.

ir = iter(ds)

그러면 iter 함수는 리스트 ds에 접근하는 도구인 iterator 객체라는 것을 생성해서 반환한다. 즉 위의 문장이 실행되면 변수 ir은 iterator 객체를 참조하는 상황이 된다. 그럼 이어서 이 객체를 전달하면서 다음과 같이 next 함수를 호출해서 리스트에 저장된 값을 하나씩 얻을 수 있다.

계속해서 next 함수를 호출하면서 iterator 객체를 전달하는 식으로 사용한다. 그러면 첫 번째 값부터 마지막 값까지 순차적으로 반환된다. 그리고 마지막 값을 얻었음에도 불구하고 다시 next 함수를호출하면 다음과 같이 예외 에러가 발생한다.

그렇다면 다시 처음부터 값을 얻으려면 어떻게 해야할까? 다음 예제에서 보이듯이 iterator 객체를 다시 얻으면 된다.

>>> ds = [1,2]
>>> ir = iter(ds)
>>> next(ir)
1
>>> next(ir)
2
>>> next(ir)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> ir = iter(ds)
>>> next(ir)
1
>>> next(ir)
2

이런 방식으로 값을 꺼내게 되면 원하는 시기에 하나씩 값을 꺼낼 수 있다. 이는 마치 아이가 먹고 싶을 때마다 사탕을 하나씩 꺼내 먹는 것과 같다. 사탕 전체를 계속 꺼내 먹을 수도 있지만 마지막 사탕은 남겨 뒀다가 내일 먹을 수도 있는 것이다. 즉 꺼내는 방법과 꺼낸 값을 처리하는 방법에 있어서도 유연성이 생긴다.

Iterable 객체와 Iterator 객체의 구분

iter 함수가 반환하는 객체를 가리켜 iterator 객체라고 하고, iterator 객체를 얻을 수 있는 리스트와 같은 객체를 가리켜 iterable 객체라 한다.

  • iterator 객체 = iter 함수에 인자로 전달 가능한 객체
  • iterable객체 = iter 함수가 생성해서 반환하는 객체

어떤 객체가 iterable 객체인지 확인하는 가장 쉬운 방법은 iter 함수에 전달해 보는 것이다. 전달 결과로 오류 없이 iterable 객체가 만들어진다면 이는 iterable 객체인 것이다. 그리고 이 두 용어를 앞으로 자주 보게 될 것이다.

  • iterable 객체를 대상으로 iter 함수를 호출해서 iterator 객체를 만든다
  • iterator 객체를 생성할 수 있는 대상이 되는 것이 iterable 객체이다

스페셜 메서드

앞서 다음과 같은 방식으로 예제를 작성했기 때문에 iter 함수와 next 함수는 객체에 속하지 않는 함수처럼 보인다.

>>> ds = [1,2,3]
>>> ir = iter(ds)
>>> next(ir)
1
>>> next(ir)
2

그러나 위 예제의 실제 함수 호출 형태는 다음과 같다.

>>> ds = [1,2,3]
>>> ir = ds.__iter__()
>>> ir.__next__()
1
>>> ir.__next__()
2

즉 iter 함수 호출은 파이썬 인터프리터에 의해 iter 메서드 호출로 이어지고,

ir = iter(ds) => ir = ds.__iter__()

next 함수 호출은 next 메서드 호출로 이어진다.

next(ir) => ir.__next__()

따라서 다음과 같은 정리가 가능하다.

  • 리스트의 iter 메서드 호출을 통해서 iterator 객체를 얻게 된다.
  • iterator 객체의 next 메서드 호출을 통해서 값을 하나씩 얻게 된다.

그리고 이렇듯 직접 호출하지 않아도 파이썬 인터프리터에 의애서 호출되는 메서드를 가리켜 다음과 같이 부른다.

  • 스페셜 메서드 (파이썬 인터프리터에 의해서 호출되는 메서드)

객체 생성 시 자동으로 호출되는 init 메서드도 스페셜 메서드이다. 그리고 이미 짐작했겠지만 스페셜 메서드는 그 나름의 이름 규칙이 있다. 이름의 앞과 뒤에 _를 두 개씩 붙여주는 것이다. 앞으로도 다양한 스페셜 메서드를 접하게 되니 지금은 그 존재를 알게 된 것에 만족하다.

Iterable 객체의 종류와 확인 방법

우리는 리스트를 통해서 iterator 객체를 얻을 수 있다. 그럼 혹시 튜플이나 문자열을 통해서도 iterator 객체를 얻을 수 있지 않을까?

>>> td = ("one", "two", "three")
>>> ir = iter(td)
>>> next(ir)
'one'
>>> next(ir)
'two'
>>> next(ir)
'three'

>>> s = "one"
>>> iter(s)
<str_iterator object at 0x7fe8e81bcb80>
>>> ir = iter(s)
>>> next(ir)
'o'
>>> next(ir)
'n'
>>> next(ir)
'e'

즉 리스트, 튜플, 문자열은 모두 iterator 객체를 반환하는 iterable 객체이다. 참고로 다음과 같이 dir 함수를 호출해서 iter 메서드가 존재하는지 확인하는 방법으로도 iterable 객체인지 아닌지 판단할 수 있다.

>>> dir([1,2,3])
['__add__', '__class__', '__class_getitem__', '__contains__', 
'__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', 
'__format__', '__ge__', '__getattribute__', '__getitem__', 
'__gt__', '__hash__', '__iadd__', '__imul__', '__init__', 
'__init_subclass__', '__iter__', '__le__', '__len__', 
'__lt__', '__mul__', '__ne__', '__new__', '__reduce__', 
'__reduce_ex__', '__repr__', '__reversed__', '__rmul__', 
'__setattr__', '__setitem__', '__sizeof__', '__str__', 
'__subclasshook__', 'append', 'clear', 'copy', 'count', 
'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

만약에 True 또는 False로 답을 듣기 원한다면 다음과 같이 hasattr 함수 호출을 통해서 iter 함수가 있는지 직접 물어볼 수도 있다.

>>> hasattr([1,2], '__iter__')
True
>>> hasattr([1,2], '__remove__')
False
>>> hasattr([1,2], '__contains__')
True

for loop와 iterable 객체

사실 for loop도 값을 하나씩 꺼내기 위해 iterable 객체를 생성해서 이것의 도움을 받는다.

for i in [1,2,3]:
    print(i, end = ' ')

# 
1 2 3

내부적으로 다음과 같은 형태로 동작한다.

ir = iter([1,2,3]) # iterator 객체를 얻는다

while True:
    try:
        i = next(ir) # iterator 객체를 통해서 값을 하나씩 꺼낸다. 
        print(i, end = ' ')
    except StopIteration: # 더 이상 꺼낼 것이 없으면, 
        break # 루프를 탈출한다

#
1 2 3

따라서 for loop의 반복 대상은 반드시 iterable 객체이어야 한다. iter 함수의 인자로 전달이 가능한 iterable 객체이어야 한다. 그래서 iterable 객체인 리스트, 튜플, 문자열은 for loop의 반복 대상이 될 수 있는 것이다. 그런데 다음 예에서 보이듯이 for loop에 iterable 객체가 아닌 iterator 객체를 두어도 정상적으로 동작을 한다.

ir = iter([1,2,3])

for i in ir:
    print(i, end = ' ')

# 
1 2 3

이렇듯 iterator 객체를 가져다 두어도 잘 동작하는 이유는 iter 함수에 iterator 객체를 전달하면 전달된 iterator 객체를 그대로 되돌려 주기 때문이다. 다음 예제에서 보이듯이 말이다.

>>> ir1 = iter([1,2,3]) # 리스트의 iterator 객체를 얻음
>>> ir2 = iter(ir1) # iterator 객체를 전달하면서 다시 iterator 객체를 얻음 
>>> ir1 is ir2 # ir1과 ir2가 참조하는 객체는 같은 객체이다
True
>>> id(ir1) # ir1이 참조하는 객체의 위치 정보 확인 
140543543982448
>>> id(ir2) # ir2이 참조하는 객체의 위치 정보 확인
140543543982448

따라서 이러한 이애를 바탕으로 다음과 같이 정리해 두는 것도 좋다.

  • iterable 객체와 iterator 객체 모두 for loop의 반복 대상이 될 수 있다.

그리고 다음과 같이 더 높은 수준으로 한 번 더 정리해주면 좋다. 이는 파이썬 코드를 다양하게 구성하는데 있어서 알고 있어야 하는 중요한 사실이기 때문이다.

  • iterable 객체가 와야 하는 위치에는 iterator 객체가 올 수 있다.

이는 iterable 객체와 마찬가지로 iterator 객체도 iter 함수의 인자가 될 수 있고 또 그 결과로 iterator 객체가 반환되기 때문이다. 비록 iter 함수에 전달된 iterator 객체가 그대로 반환되는 것이지만 마

profile
지뢰찾기 개발자

0개의 댓글