Computer Engineering/Fluent Python 정리

Fluent Python Chapter 14. 반복형, 반복자, 제너레이터

jordan.bae 2022. 2. 27. 13:58

Introduction

14장에서는 데이터를 처리할 때 항상 다루는 반복에 대해서 다룬다. Iterable, Iterator, Generator에 대해서 다루면서 iter() 내장 함수가 동작하는 방법을 살펴 본 후에 Generator가 데이터를 어떻게 느긋하게 가져오는지 살펴본다. 그리고 표준 라이브러리에서 제공하는 제너레이터를 살펴보면서 파이썬에서 효율적으로 데이터를 반복해서 가져와서 처리하는 방법들을 살펴본다.

 

책 정리를 시작한 이유

책 정리를 시작한 이유

Chapter1의 Introduction 부분에서 이야기 한 것처럼 지난 5년간 다양한 언어나 프레임워크 및 프로그램을 공부하고 이용하여 소프트웨어를 개발했는데 이것저것 하다 보니 자주 사용하는 언어임에도 불구하고 파이썬을 잘 활용하고 있느냐에 대한 답변을 자신 있게 하기 어렵다고 느껴서 Fluent Python이라는 책을 공부하며 정리하고 있습니다. 올해에는 새로운 기술들도 좋지만 기존에 활용하던 언어나 프레임워크 그리고 소프트웨어를 더 잘 사용할 수 있도록 깊게 공부할 수 있는 한 해를 만들고 싶은 소망을 가지고 있습니다. 21장까지 정리를 성공하고 맛있는 걸 먹으면서 스스로 축하할 날이 어서 왔으면 좋겠네요! 혹시, 제가 정리한 글이 괜찮으시면 구독 하시면 다음편이 나오면 바로 알림을 받으실 수 있습니다.

 

지난 chapter 정리한 포스팅

Fluent Python Chapter 1. 파이썬 데이터 모델 (Feat. 일관성 있는 언어)

Fluent Python Chapter 2-1. Sequence (Sequence의 분류, listcomp, generator, tuple, slicing

Fluent Python Chapter 2-2. Sequence (bisect, deque, memoryview)

Fluent Python Chapter 3. Dictionary (feat. hashtable)

Fluent Python Chapter 4. 텍스트와 바이트 (feat. 깨지지마라..)

Fluent Python Chapter 5. 일급 함수

Fluent Python Chapter 6. 일급 함수 디자인 패턴

Fluent Python Chapter 7. 함수 데커레이터와 클로저 (feat. 메타프로그래밍)

Fluent Python Chapter 8. 객체 잠조, 가변성, 재활용

Fluent Python Chapter 9. 파이썬스러운 객체

Fluent Python Chapter 10. 시퀀스 해킹, 해시, 슬라이스

Fluent Python Chapter 11. 인터페이스: 프로토콜에서 ABC까지

Fluent Python Chapter 12. 내장 자료형 상속과 다중 상속

Fluent Python Chapter 13. 연산자 오버로딩(feat. 제대로 하기)

 

iter 함수의 동작

먼저 아래의 Sentence class 코드를 보자.

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:

    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)  # <1>

    def __getitem__(self, index):
        return self.words[index]  # <2>

    def __len__(self):  # <3>
        return len(self.words)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)  # <4>
        
 
s = Sentence('i like python. because it is really practical')
for w in s:
   print(w)
i
like
python
...
practical

Sequece 객체가 반복 가능한(iterable) 이유는 iter()함수의 구현에 숨어있다.

iter() 내장 함수는 다음 과정을 수행.

1. 객체가 __iter__() 메서드를 구현하는지 확인하고, 이 메서드를 호출해서 반복자를 가져온다.

2. __iter__()메서드가 구현되어 있지 않지만 __getitem__() 이 구현되어 있다면, 파이썬은 인덱스 0에서 시작해서 항목을 순서대로 가져오는 반복자를 생성한다.

3. 이 과정이 모두 실패하면 파이썬은 'TypeError: 'C' object is not iterable'이라는 메시지와 함께 TypeError가 발생한다.

여기서도 전에 살펴봤던 파이썬의 데이터 모델이 여러 프로토콜과 최대한 협력하려는 철학이 드러난다. 구스 타이핑으로 체크한다면 이 클래스의 객체는 iterable은 아니다.

from collections import abc

issubclass(Sentence, abc.Iterable)
False

그렇기 때문에 우리가 duck typing 스타일로 코딩한다면 iter(x)를 호출한 후에 TypeError를 핸들링하는 것이다. 

# duck typing

try:
    iter(x)
except TypeError e:
    blabla
    
    
# goose typing
if isinstance(x, abc.Iterable):
    blabla

 

 

Iterable과 Iterator

Iterable (반복형)

iter()내장 함수가 iterator(반복자)를 return하는 객체이다. 즉, 정상적으로 __iter__() 메서드를 구현했거나 __getitem__()메서드를 구현한 객체가 반복형 객체이다. 

Iterator (반복자)

Iterable한 객체의 iter() 함수가 return하는 객체로 __next__(), __iter__() 매직메서드 인터페이스를 구현한 객체이다. __iter__() 매직메서드를 구현함으로써 Iterable이기도 하다.
s= 'abc'
# iterable한 객체는 iter함수가 iterator를 return 한다.
iterator = iter(s)


while True:
    try:
        print(next(iterator))
    except StopIteration:
        del iterator
        break

a
b
c

 

책에서 소개한 abc.Iterator 클래스 일부를 살펴보면 Iterator를 좀 더 잘 이해할 수 있다.

class Iterator(Iterable):
    
    __slots__ = ()
    
    @abcstractmethod
    def __next__(self):
        '''반복자에서 다음 항목을 반환한다. 항목이 소진되면 StopIteration예외를 발생시킨다.'''
        raise StopIteration
        
    def __iter__(self):
        return self
    
    @classmethod
    def __subclasshook__(cls, C):
        if cls is Iterator:
            if (any("__next__" in B.dict for B in C.__mro__) and
                any("__iter__" in B.dict for B in C.__mro__))
                return True
        return NotImplementedError

 

위에 설명처럼 Iterator는 Iterable이지만 Iterable은 Iterator가 아니다.

위에서 살펴본 Sentence clsss를 Iterator로 만드는 것은 좋은 생각이 아니다. 반복자 패턴은 아래와 같은 경우에 사용하는 것을 추천한다고 한다.

- 집합 객체의 내부 표현을 노출시키지 않고 내용에 접근하는 경우

- 집합 객체의 다중 반복을 지원하는 경우

- 다양한 집합 구조체를 반복하기 위한 통일된 인터페이스를 제공하는 경우

 

다중 반복이라는 것은 해당 객체를 여러 번 반복하는 것을 의미한다. 여러 번 반복해야 하기 때문에 각 반복자는 독립된 객체이다. 위에서 iterator에서 next 함수를 한 번 호출 하면 앞의 element로 되돌아갈 수 없고 새로운 iterator를 만들어야 한다. 즉, iter 함수를 호출 할 때 마다 독립적인 반복자가 새로 만들어져 한다는 것을 의미한다.  아래처럼 구현 할 수 있지만 이는 파이썬스럽지 않다.

class Sentence:

    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):  # <1>
        return SentenceIterator(self.words)  # <2>


class SentenceIterator:

    def __init__(self, words):
        self.words = words  # <3>
        self.index = 0  # <4>

    def __next__(self):
        try:
            word = self.words[self.index]  # <5>
        except IndexError:
            raise StopIteration()  # <6>
        self.index += 1  # <7>
        return word  # <8>

    def __iter__(self):  # <9>
        return self

Generator를 살펴보면서 위의 코드를 파이썬스러운 코드로 리팩토링 한다.

 

 

Generator

위의 코드를 파이썬스럽게 구현하려면 SequenceIterator 클래스를 구현해서 사용하는 대신에 제너레이터 함수를 사용한다.

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:

    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        for word in self.words:  # <1>
            yield word  # <2>
        return  # <3>

 

yield 키워드를 가진 모든 함수는 gernerator 함수로 generator 함수는 generator를 return한다. (그래서 generator factory이기도 하다.) 위의 코드가 리팩토링된 코드이므로 Sentence의 __iter__()함수가 iterator를 반환한다는 것을 의미한다. generator는 iterator다라는 것을 알 수 있다. 즉, __next__(), __iter__() 인터페이스가 구현되어 있다.

 

def gen_ex():
    yield 2
    yield 3
    return
a = gen_ex()


print(next(a))
print(next(a))
print(next(a))
2
3
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
Input In [12], in <module>
      4 print(next(a))
      5 print(next(a))
----> 6 print(next(a))

StopIteration:

'gen_ex' generator 함수는 generator를 return 하는데 next() 함수를 구현 했다. (StopIteration 예외도 발생.)

Generator는 next()를 generator 객체에 호출하면 함수 본체에 있는 다음 yield로 진행되고 yield가 반환하는 값을 생성해서 next()에 return한다. 즉, Generator는 값을 모두 생성하고 반환하는 것이 아니라, 필요할 때 반환해야 하는 객체만 생성해서 반환한다.

 

위의 Sentence 클래스 코드를 조금 더 느긋(lazy)하게 구현해본다. 왜냐하면 초기화 과정에서 이미 값을 모두 생성하기 때문이다.

re.finditer() 함수는 re.findall()의 lazy한 버전이다. 리스트를 리턴하는게 아니라 re.MatchObject 객체를 생성하는 generator를 반환한다.

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:

    def __init__(self, text):
        self.text = text  # <1>

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        for match in RE_WORD.finditer(self.text):  # <2>
            yield match.group()  # <3>

 

 

Generator 표현식

위에서 우리는 generator 함수를 통해서 generator를 만들었다.

이번에는 Generator 표현식을 통해서 generator를 만든다. 앞에 자료 구조 chapter들에서 살짝 살펴봤었다.

list comprehension과 generator comprehension을 살펴보자. list_comp 객체는 생성할 때 모든 코드도 수행되서 list를 생성한다. gen_comp는 generator객체를 만들어서 반복하면서 순서에 맞게 하나씩 객체를 생성한다.

def gen_ex_2():
    print('print A')
    yield 'A'
    print('print B')
    yield 'B'
    print('end')

# list를 생성
list_comp = [x*3 for x in gen_ex_2()]
print A
print B
end

# generator를 생성
gen_comp = (x*3 for x in gen_ex_2())
for i in gen_comp:
    print(i)
print A
AAA
print B
BBB
end

위의 Sentence 코드의 __iter__(self) 함수를 generator 표현식으로 고쳐보면 아래와 같다.

...
    def __iter__(self):
        return (match.group() for match in RE_WORD.finditer(self.text))

 

 

표준 라이브러리의 generator 함수

개인적으로 유용하게 쓸 것 같은 함수들만 정리해본다.


필터링 generator함수

모듈 함수 설명
itertools compress(it, selector_it) 두 개의 반복형을 병렬로 체크하면서 selector_it의 원소가 참일 때만 it에서 항목을 생성한다.
itertools dropwhile(predicate, it) predicate가 참된 값인 동안 항목들을 지나가면서 it를 소비한 후, 추가 검사 없이 남아 있는 항목을 모두 생성한다.

 

매핑 generator 함수

itertools accumulate(it, [func]) 누적된 합게를 구한다. func를 제공하면, 처음 두개의 항목에 func를 적용한 결과를 첫번째 값으로 생성하며 it을 반복한다.
itertools starmap(func, it) it의 각 항목에 func를 적용해서 결과를 생성한다.

 

입력된 여러 입력 반복형을 병합하는 제너레이터 함수

itertools chain(it1, ..., itN) it1의 모든 항목을 생성한 후, 나머지 반복형의 항목을 차례대로 생성한다.
itertools cain.from_iterable(it) it에서 생성된 반복형 객체의 모든 항목을 생성한다. (ex. it은 반복형의 리스트)
내장 zip(it1, ..., itN) 각 it의 항목을 병렬로 소비해서 N-튜플을 생성하고, 어느 하나의 it가 소모되면 조용히 중단된다.
itertools zip_longest(it1,....,itN, fillvalue=None) 각 it의 항목을 병렬로 소비해서 N 튜플을 생성하다. 최종 it가 소모될 때까지 빈 값을 fillvalue로 채워가며 생성한다.

 

입력된 항목 하나를 여러 개로 확장하는 제너레이터 함수

itertools combinations(it, outlen) out_len개의 조합을 생성
itertools combinations_with_replacement(it, outlen) 반복된 항목들의 조합을 포함해서, it로 생성된 항목에서 out_lenrodml 조합을 생성
itertools cyce(it) 각 항목의 사본을 저장한 후, 항목을 무한히 반복한다.
itertools permutations(it, out_len=None) it로 생성된 항목해서 out_len개 항몫의 조합을 생성한다.
itertools repeat(item, [times]) times를 지정하면 times만큼, 아니면 주어진 item을 무한히 반복 생성.

 

yield from

yield from은 파이썬 3.3에서 도입된 구문이다.

yield from은 다른 제너레이터에서 생성된 값을 상위 제너레이터 함수가 생성해야 할 때 전통적으로 중첩된 for루프를 대체하는 구문이다.

# itertools.chain은 C로 구현되어 있으나 예제를 위해서 python으로 구현
def chain(*iterable):
    for it in iterables:
        for i in it:
            yield i

# yield from으로 대체
def chain(*iterable):
    for it in iterables:
        yield i from it

 

반복형을 reduce하는 함수

반복형을 받아서 하나의 값을 반환하는 함수이다.

이 중 all()과 any()는 short-circuit evaluation함수로 중간에 결과가 결정되면 반복을 중단한다. 

 

 

마무리

이번 장에서는 Iterable, Iterator, Generator 객체를 반복해서 소비하는 객체에 대해서 살펴봤습니다. 특히, Generator에는 lazy하게 반복할 수 있어서 메모리를 효율적으로 사용합니다. 이런 점을 기억하고 필요에 따라서 내부 라이러브에 구현된 generator 함수들을 잘 사용하면 효율적으로 프로그래밍 할 수 있을 것 같습니다.
읽어주셔서 감사합니다.

 

 

Reference

- Fluent Python Chapter 14

- https://github.com/fluentpython/example-code/tree/master/14-it-generator

 

반응형