4. Iterators and Generators

4.1. Manually Consuming an Iterator

P : for문을 쓰지 않고 순회 가능한 항목에 접근하고 싶다.

S : 반복자로 순회하며 next() 함수를 사용하고 StopIteration 예외를 처리한다.

with open('/etc/passwd') as f:
    try:
        while True:
            line = next(f)
            print(line, end='')
    except StopIteration:
        pass

>>> items = [1, 2, 3]
>>> # Get the iterator
>>> it = iter(items)     # Invokes items.__iter__()
>>> # Run the iterator
>>> next(it)             # Invokes it.__next__()
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

4.2. Delegating Iteration

P : 순회 가능한 객체를 담은 사용자 정의 컨테이너를 만들었는데, 이 컨테이너를 순회할 수 있는 반복자를 만들고 싶다.

S : __iter__() 메소드를 정의하면 된다.

class Node:
    def __init__(self, value):
        self._value = value
        self._children = []

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def add_child(self, node):
        self._children.append(node)

    def __iter__(self):
        return iter(self._children)

# Example
if __name__ == '__main__':
    root = Node(0)
    child1 = Node(1)
    child2 = Node(2)
    root.add_child(child1)
    root.add_child(child2)
    for ch in root:
        print(ch)
    # Outputs Node(1), Node(2)

4.3. Creating New Iteration Patterns with Generators

P : 커스텀 순회 패턴을 만들고 싶다.

S : 제네레이터 함수를 만든다.

def frange(start, stop, increment):
    x = start
    while x < stop:
        yield x
        x += increment

>>> for n in frange(0, 4, 0.5):
...     print(n)
...
0
0.5
1.0
1.5
2.0
2.5
3.0
3.5
>>> list(frange(0, 1, 0.125))
[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875]

내부에 yield문이 있으면 함수가 제네레이터가 된다. 이는 순회에 의한 “다음” 연산에 응답하기 위해서만 실행된다.

>>> def countdown(n):
...     print('Starting to count from', n)
...     while n > 0:
...             yield n
...             n -= 1
...     print('Done!')
...

>>> # Create the generator, notice no output appears
>>> c = countdown(3)
>>> c
<generator object countdown at 0x1006a0af0>

>>> # Run to first yield and emit a value
>>> next(c)
Starting to count from 3
3

>>> # Run to the next yield
>>> next(c)
2

>>> # Run to next yield
>>> next(c)
1

>>> # Run to next yield (iteration stops)
>>> next(c)
Done!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

4.4. Implementing the Iterator Protocol

P : 커스텀 오브젝트에서 순회를 지원하고 싶다.

S : 제네레이터 함수를 사용한다. 아래는 트리를 깊이 우선 탐색(DFS)로 순회한다.

class Node:
    def __init__(self, value):
        self._value = value
        self._children = []

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def add_child(self, node):
        self._children.append(node)

    def __iter__(self):
        return iter(self._children)

    def depth_first(self):
        yield self
        for c in self:
            yield from c.depth_first()

# Example
if __name__ == '__main__':
    root = Node(0)
    child1 = Node(1)
    child2 = Node(2)
    root.add_child(child1)
    root.add_child(child2)
    child1.add_child(Node(3))
    child1.add_child(Node(4))
    child2.add_child(Node(5))

    for ch in root.depth_first():
        print(ch)

4.5. Iterating in Reverse

P : 역방향 순회를 하고 싶다.

S : reversed()를 사용한다. __reversed__() 메소드를 구현해 커스텀 클래스를 역방향 순회할 수도 있다.

>>> a = [1, 2, 3, 4]
>>> for x in reversed(a):
...     print(x)
...
4
3
2
1

class Countdown:
    def __init__(self, start):
        self.start = start

    # Forward iterator
    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

    # Reverse iterator
    def __reversed__(self):
        n = 1
        while n <= self.start:
            yield n
            n += 1

4.6. Defining Generator Functions with Extra State

P : 추가 상태를 가진 제네레이터 함수를 정의하고 싶다.

S : __iter__() 메소드에 제네레이터 함수 코드를 넣어 구현한다.

from collections import deque

class linehistory:
    def __init__(self, lines, histlen=3):
        self.lines = lines
        self.history = deque(maxlen=histlen)

    def __iter__(self):
        for lineno, line in enumerate(self.lines,1):
            self.history.append((lineno, line))
            yield line

    def clear(self):
        self.history.clear()

with open('somefile.txt') as f:
     lines = linehistory(f)
     for line in lines:
         if 'python' in line:
             for lineno, hline in lines.history:
                 print('{}:{}'.format(lineno, hline), end='')

4.7. Taking a Slice of an Iterator

P : 반복자가 만드는 데이터의 일부만 가져오고 싶다.

S : itertools.islice() 함수를 사용한다.

>>> def count(n):
...     while True:
...             yield n
...             n += 1
...
>>> c = count(0)
>>> c[10:20]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable

>>> # Now using islice()
>>> import itertools
>>> for x in itertools.islice(c, 10, 20):
...     print(x)
...
10
11
12
13
14
15
16
17
18
19

반복자와 제네레이터의 슬라이스는 실제로 슬라이스를 가져오는 것이 아니라 시작 인덱스까지 모든 항목을 버린 뒤 마지막 항목까지의 항목을 생성하는 식으로 구현된다. 반복자를 되감을 수는 없기 때문에 되감기를 원하면 데이터를 리스트로 변환하는 것이 낫다.

4.8. Skipping the First Part of an Iterable

P : 항목을 순회할 때 처음 몇 부분을 건너뛰고 싶다.

S : itertools.dropwhile(), itertools.islice()를 사용한다. 이 방법은 순회 가능한 모든 객체, 제네레이터나 파일 등에도 적용 가능하다.

>>> from itertools import dropwhile
>>> with open('/etc/passwd') as f:
...     for line in dropwhile(lambda line: line.startswith('#'), f):
...          print(line, end='')
...

>>> from itertools import islice
>>> items = ['a', 'b', 'c', 1, 4, 10, 15]
>>> for x in islice(items, 3, None):
...     print(x)
...

4.9. Iterating Over All Possible Combinations or Permutations

P : 항목들의 모든 순열이나 조합을 순회하고 싶다.

S : itertools.permutations(), itertools.combinations()를 사용한다. 중복을 없애려면 itertools.combinations_with_replacement()를 사용한다.

>>> items = ['a', 'b', 'c']
>>> from itertools import permutations
>>> for p in permutations(items):
...     print(p)
...
>>> from itertools import combinations
>>> for c in combinations(items, 3):
...     print(c)
...
('a', 'b', 'c')

4.10. Iterating Over the Index-Value Pairs of a Sequence

P : 항목을 순회하면서 인덱스도 알고 싶다.

S : enumerate()를 사용한다.

>>> my_list = ['a', 'b', 'c']
>>> for idx, val in enumerate(my_list, 1):
...     print(idx, val)
...
1 a
2 b
3 c

파일을 순회하며 라인 번호를 아는데도 쓸 수 있다.

def parse_data(filename):
    with open(filename, 'rt') as f:
         for lineno, line in enumerate(f, 1):
             fields = line.split()
             try:
                 count = int(fields[1])
                 ...
             except ValueError as e:
                 print('Line {}: Parse error: {}'.format(lineno, e))

튜플의 시퀀스를 순회할 때는 다음과 같이 써야 한다.

data = [ (1, 2), (3, 4), (5, 6), (7, 8) ]

# Correct!
for n, (x, y) in enumerate(data):
    ...

# Error!
for n, x, y in enumerate(data):
    ...

4.11. Iterating Over Multiple Sequences Simultaneously

P : 여러 시퀀스를 동시에 순회하고 싶다.

S : zip()을 사용한다.

>>> xpts = [1, 5, 4, 2, 10, 7]
>>> ypts = [101, 78, 37, 15, 62, 99]
>>> for x, y in zip(xpts, ypts):
...     print(x,y)
...
1 101
5 78
4 37
2 15
10 62
7 99
>>>

여러 시퀀스의 길이가 다를 경우 가장 짧은 시퀀스의 길이에 맞춰진다. 그것을 원하지 않으면 zip_longest()를 사용한다.

>>> from itertools import zip_longest
>>> for i in zip_longest(a,b):
...     print(i)
...
(1, 'w')
(2, 'x')
(3, 'y')
(None, 'z')
>>> for i in zip_longest(a, b, fillvalue=0):
...     print(i)
...
(1, 'w')
(2, 'x')
(3, 'y')
(0, 'z')

4.12. Iterating on Items in Separate Containers

P : 여러 컨테이너를 순서대로 한번에 순회하고 싶다.

S : itertools.chain()을 사용한다.

>>> from itertools import chain
>>> a = [1, 2, 3, 4]
>>> b = ['x', 'y', 'z']
>>> for x in chain(a, b):
...     print(x)
...
1
2
3
4
x
y
z
>>>

4.13. Creating Data Processing Pipelines

P : 데이터를 연속적으로 처리하는 파이프라인을 만들고 싶다.

S : 제네레이터를 사용한다.

import os
import fnmatch
import gzip
import bz2
import re

def gen_find(filepat, top):
    '''
    Find all filenames in a directory tree that match a shell wildcard pattern
    '''
    for path, dirlist, filelist in os.walk(top):
        for name in fnmatch.filter(filelist, filepat):
            yield os.path.join(path,name)

def gen_opener(filenames):
    '''
    Open a sequence of filenames one at a time producing a file object.
    The file is closed immediately when proceeding to the next iteration.
    '''
    for filename in filenames:
        if filename.endswith('.gz'):
            f = gzip.open(filename, 'rt')
        elif filename.endswith('.bz2'):
            f = bz2.open(filename, 'rt')
        else:
            f = open(filename, 'rt')
        yield f
        f.close()

def gen_concatenate(iterators):
    '''
    Chain a sequence of iterators together into a single sequence.
    '''
    for it in iterators:
        yield from it

def gen_grep(pattern, lines):
    '''
    Look for a regex pattern in a sequence of lines
    '''
    pat = re.compile(pattern)
    for line in lines:
        if pat.search(line):
            yield line

lognames = gen_find('access-log*', 'www')
files = gen_opener(lognames)
lines = gen_concatenate(files)
pylines = gen_grep('(?i)python', lines)
for line in pylines:
    print(line)

lognames = gen_find('access-log*', 'www')
files = gen_opener(lognames)
lines = gen_concatenate(files)
pylines = gen_grep('(?i)python', lines)
bytecolumn = (line.rsplit(None,1)[1] for line in pylines)
bytes = (int(x) for x in bytecolumn if x != '-')
print('Total', sum(bytes))

4.14. Flattening a Nested Sequence

P : 중첩된 시퀀스를 풀어헤치고 싶다.

S : yield 문을 포함한 재귀 제네레이터를 사용하면 된다.

from collections import Iterable

def flatten(items, ignore_types=(str, bytes)):
    for x in items:
        if isinstance(x, Iterable) and not isinstance(x, ignore_types):
            yield from flatten(x, ignore_types)
        else:
            yield x

items = [1, 2, [3, 4, [5, 6], 7], 8]

# Produces 1 2 3 4 5 6 7 8
for x in flatten(items):
    print(x)

4.15. Iterating in Sorted Order Over Merged Sorted Iterables

P : 두 정렬된 순회 가능한 시퀀스를 정렬된 채 병합하고 싶다.

S : heapq.merge()를 사용한다.

>>> import heapq
>>> a = [1, 4, 7, 10]
>>> b = [2, 5, 6, 11]
>>> for c in heapq.merge(a, b):
...     print(c)
...
1
2
4
5
6
7
10
11

4.16. Replacing Infinite while Loops with an Iterator

P : while문을 순회로 변경하고 싶다.

S : iter()와 람다식에 종료 조건을 줘서 사용할 수 있다.

CHUNKSIZE = 8192

def reader(s):
    while True:
        data = s.recv(CHUNKSIZE)
        if data == b'':
            break
        process_data(data)

def reader2(s):
    for chunk in iter(lambda: s.recv(CHUNKSIZE), b''):
        process_data(chunk)

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중