본문 바로가기

Python study

제너레이터 (Generator)

WordIterator

단어별로 반복하는 클래스는 주어진 텍스트에서 단어를 추출하고, 이를 반복 가능한 형태로 제공하는 기능을 하는 클레스를 만들기 위해 re.findall 함수를 사용해 텍스트에서 단어를 추출하고, 이를 반복 가능한 객체로 반환하는 클래스를 만들어 보겠다.

import re

class WordIterator:
    def __init__(self, text):
        self.words = re.findall(r'\\b\\w+\\b', text)

    def __getitem__(self, index):
        return self.words[index]

    def __len__(self):
        return len(self.words)

    def __repr__(self):
        return f"WordIterator({self.words})"

# Example usage
text = "This is a sample text with several words."
word_iterator = WordIterator(text)
print(len(word_iterator))  # 8
print(word_iterator)  # WordIterator(['This', 'is', 'a', 'sample', 'text', 'with', 'several', 'words'])
for i in range(len(word_iterator)):
    print(word_iterator[i])

'''
This
is
a
sample
text
with
several
words
'''

 

WordIterator가 반복이 가능한 이유

WordIterator 클래스가 반복 가능한 이유를 설명하려면, Python의 이터러블(iterable)과 이터레이터(iterator) 프로토콜에 대해 이해해야 한다. 이 프로토콜은 Python에서 객체를 반복 가능하게 만드는 기본적인 메커니즘이다.

이터러블(Iterable) 객체

이터러블 객체는 iter 메서드를 구현하는 객체이다. 이 메서드는 이터레이터 객체를 반환해야 한다. 이터러블 객체의 예로는 리스트, 튜플, 문자열 등이 있다. 이 객체들은 모두 iter 메서드를 가지고 있다.

이터레이터(Iterator) 객체

이터레이터 객체는 두 가지 메서드를 구현하는 객체이다:

  1. iter 메서드: 자기 자신(self)을 반환해야 한다. 이는 이터레이터가 이터러블 객체로서 반복문에 사용될 수 있게 한다.
  2. next 메서드: 다음 값을 반환하고, 더 이상 반환할 값이 없으면 StopIteration 예외를 발생시켜야 한다.

WordIterator 클래스의 경우

WordIterator 클래스는 getitem 메서드를 통해 간접적으로 반복 가능하게 만들었다. Python은 iter 메서드를 정의하지 않고 getitem 메서드를 정의한 객체를 특별히 취급하여, 인덱스를 사용해 객체를 반복할 수 있도록 한다.

iter 함수의 역할

Python의 iter 함수는 객체를 반복 가능한 객체로 변환하는 데 사용된다. iter 함수는 다음과 같은 방식으로 작동한다:

  1. 객체가 iter 메서드를 구현한 경우, iter 함수는 이 메서드를 호출하여 이터레이터를 반환한다.
  2. 객체가 iter 메서드를 구현하지 않고, getitem 메서드를 구현한 경우, iter 함수는 인덱스 0부터 시작하여 순차적으로 값을 반환하는 이터레이터를 생성한다. 이 과정은 인덱스가 더 이상 유효하지 않을 때까지 계속된다.

WordIterator 클래스의 작동 방식

WordIterator 클래스는 getitem 메서드를 사용하여 반복을 지원한다. 따라서 iter 함수는 이 클래스의 인스턴스에 대해 호출될 때, 인덱스 0부터 시작하여 getitem 메서드를 사용해 값을 가져오고, 더 이상 값이 없을 때 StopIteration 예외를 발생시킨다.

즉, WordIterator 클래스가 반복 가능한 이유는 Python의 이터러블 및 이터레이터 프로토콜을 준수하기 때문이다. getitem 메서드를 구현함으로써, 이 클래스는 iter 함수에 의해 이터러블 객체로 인식된다. 이터러블 객체는 for 루프와 같은 반복문에서 사용할 수 있으며, 이는 Python의 유연하고 강력한 반복 프로토콜 덕분이다.

 

반복형과 반복자

반복형(Iterable)과 반복자(Iterator)는 Python의 반복 프로토콜을 이해하는 데 중요한 개념이다. 이 두 가지 개념은 Python에서 데이터 컬렉션을 순회하는 방식에 대한 기본적인 이해를 제공한다.

반복형(Iterable)

반복형(Iterable)은 iter 메서드를 구현한 객체를 말한다. iter 메서드는 이터레이터 객체를 반환한다. 반복형 객체의 예로는 리스트, 튜플, 문자열, 딕셔너리, 세트 등이 있다. 반복형 객체는 반복문(예: for 루프)에서 순회할 수 있다.

예제: 리스트

my_list = [1, 2, 3, 4, 5]
for item in my_list:
    print(item)

이 예제에서 my_list는 반복형 객체이다. for 루프는 my_list의 각 요소를 순회한다.

반복자(Iterator)

반복자(Iterator)는 __iter__와 next 메서드를 구현한 객체를 말한다. iter 메서드는 반복자 객체 자체를 반환하고, next 메서드는 다음 값을 반환한다. 만약 더 이상 반환할 값이 없으면 StopIteration 예외를 발생시킨다.

예제: 사용자 정의 반복자

class MyIterator:
    def __init__(self, data):
        self.data = data
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index < len(self.data):
            result = self.data[self.index]
            self.index += 1
            return result
        else:
            raise StopIteration

my_iter = MyIterator([1, 2, 3, 4, 5])
for item in my_iter:
    print(item)

이 예제에서 MyIterator 클래스는 반복자 객체를 정의한다. iter 메서드는 객체 자체를 반환하고, next 메서드는 다음 값을 반환하거나 더 이상 값이 없을 때 StopIteration 예외를 발생시킨다.

반복형과 반복자의 관계

반복형과 반복자는 밀접하게 연관되어 있다. 반복형 객체는 iter 메서드를 통해 반복자를 반환하며, 반복자는 next 메서드를 통해 반복형 객체의 요소를 하나씩 반환한다. 반복형 객체는 반복자 프로토콜을 통해 요소를 순회할 수 있다.

반복형과 반복자의 상호작용

  1. 반복형 객체는 반복자를 생성:
    • iter() 함수를 호출하면 반복형 객체의 iter 메서드가 호출되고, 반복자를 반환한다.
    my_list = [1, 2, 3]
    iterator = iter(my_list)
    
  2. 반복자는 순회를 수행:
    • 반복자의 next 메서드를 호출하면 반복형 객체의 다음 요소를 반환한다. 더 이상 요소가 없으면 StopIteration 예외가 발생한다.
    print(next(iterator))  # 출력: 1
    print(next(iterator))  # 출력: 2
    print(next(iterator))  # 출력: 3
    # 다음 호출은 StopIteration 예외를 발생시킨다.
  • 반복형(Iterable): iter 메서드를 구현하여 반복자를 반환하는 객체이다. 반복형 객체는 for 루프와 같은 반복문에서 순회할 수 있다.
  • 복자(Iterator): __iter__와 next 메서드를 구현한 객체이다. 반복자는 next 메서드를 통해 다음 요소를 반환하며, 더 이상 요소가 없으면 StopIteration 예외를 발생시킨다.

반복형과 반복자의 관계는 Python의 반복 프로토콜을 이해하는 데 중요한 부분이다. 반복형 객체는 iter() 함수를 통해 반복자를 생성하고, 반복자는 next() 함수를 통해 요소를 순회한다.

 

WordIterator (반복자와 반복형)

반복형(Iterable)과 반복자(Iterator)로 나누어 WordIterator 클래스를 구현하겠다. 이를 위해 두 개의 클래스를 정의한다: 하나는 반복형 역할을 하고, 다른 하나는 실제 반복자 역할을 한다.

  1. WordIterable: 반복형 클래스로, 텍스트를 받아 반복자를 반환한다.
  2. WordIterator: 반복자 클래스로, 텍스트의 단어를 하나씩 반환한다.
import re

class WordIterator:
    def __init__(self, words):
        self.words = words
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index < len(self.words):
            word = self.words[self.index]
            self.index += 1
            return word
        else:
            raise StopIteration

class WordIterable:
    def __init__(self, text):
        self.words = re.findall(r'\\b\\w+\\b', text)

    def __iter__(self):
        return WordIterator(self.words)

# Example usage
text = "This is a sample text with several words."
word_iterable = WordIterable(text)
for word in word_iterable:
    print(word)

코드 설명

  1. WordIterator 클래스 (반복자):
    • init 메서드: 단어 리스트와 현재 인덱스를 초기화한다.
    • iter 메서드: 자기 자신을 반환한다. 이 메서드는 이터레이터가 이터러블 객체로서 반복문에 사용될 수 있게 한다.
    • next 메서드: 현재 인덱스의 단어를 반환하고, 인덱스를 증가시킨다. 더 이상 단어가 없으면 StopIteration 예외를 발생시킨다.
  2. WordIterable 클래스 (반복형):
    • init 메서드: 주어진 텍스트에서 단어를 추출하여 리스트에 저장한다.
    • iter 메서드: WordIterator 객체를 반환합니다. 이를 통해 WordIterable 객체가 반복 가능해진다.

이 예제에서는 WordIterable 객체를 생성하고, 이를 for 루프에서 사용하여 각 단어를 출력한다. WordIterable 클래스의 iter 메서드는 WordIterator 객체를 반환하며, 이 객체는 next 메서드를 통해 각 단어를 하나씩 반환한다.

 

WordIterator (제너레이터 함수)

제너레이터 함수를 이용하여 WordIterator를 구현하면 메모리 효율을 개선하고 코드의 간결성을 유지할 수 있다. 제너레이터 함수는 yield 키워드를 사용하여 값을 반환하며, 호출될 때마다 현재 상태를 유지하면서 실행을 일시 중지한다. 이를 통해 큰 데이터 셋을 처리할 때도 메모리 사용량을 최소화할 수 있다.

다음은 제너레이터 함수를 이용한 WordIterator 구현이다:

import re

class WordIterable:
    def __init__(self, text):
        self.text = text

    def __iter__(self):
        return self.word_generator()

    def word_generator(self):
        words = re.findall(r'\\b\\w+\\b', self.text)
        for word in words:
            yield word

# Example usage
text = "This is a sample text with several words."
word_iterable = WordIterable(text)
for word in word_iterable:
    print(word)

코드 설명

  1. WordIterable 클래스:
    • init 메서드: 주어진 텍스트를 저장한다.
    • iter 메서드: 제너레이터 함수를 호출하여 제너레이터 객체를 반환한다.
    • word_generator 메서드: 제너레이터 함수로, 정규 표현식을 사용하여 텍스트에서 단어를 추출하고, 각 단어를 yield 키워드를 사용하여 반환한다.

예제 사용법

  • WordIterable 객체를 생성하고, 이를 for 루프에서 사용하여 각 단어를 출력한다.
  • 제너레이터 함수 word_generator는 호출될 때마다 다음 단어를 yield 합니다. 반복이 끝나면 StopIteration 예외가 자동으로 발생하여 반복을 종료한다.

이 구현 방식은 메모리 사용을 최적화하며, 매우 큰 텍스트를 처리할 때도 효율적으로 동작한다. 또한 코드가 간결하고 유지보수하기 쉽다.

 

제너레이터 함수

제너레이터 함수란?

제너레이터 함수는 일반 함수와 유사하지만, 값을 반환할 때 return 키워드 대신 yield 키워드를 사용한다. 제너레이터 함수는 호출될 때마다 하나의 값을 반환하고, 실행 상태를 유지하면서 중지된다. 다음에 다시 호출되면 중지된 지점에서 실행을 재개한다. 이를 통해 메모리 효율성을 높이고, 매우 큰 데이터셋을 다룰 때 유용하게 사용할 수 있다.

제너레이터 함수의 작동 방식

  1. 정의와 호출:
    • 제너레이터 함수는 yield 키워드를 포함한 함수로 정의된다.
    • 호출되면 제너레이터 객체를 반환하지만, 함수의 본문은 실행되지 않는다.
    def my_generator():
        yield 1
        yield 2
        yield 3
    
    gen = my_generator()  # 제너레이터 객체 생성
    
    print(next(gen))  # 출력: 1
    print(next(gen))  # 출력: 2
    print(next(gen))  # 출력: 3
    print(next(gen))  # StopIteration 예외 발생
    
  2. 제너레이터 객체:
    • 제너레이터 함수를 호출하면 제너레이터 객체가 생성된다. 이 객체는 이터레이터 프로토콜을 구현하며, __iter__와 next 메서드를 갖고 있다.
  3. 첫 번째 next() 호출:
    • next() 함수를 호출하면 제너레이터 함수가 실행을 시작하고, 첫 번째 yield 문을 만날 때까지 실행된다. 이때 yield된 값이 반환된다.
  4. 연속적인 next() 호출:
    • next() 함수가 호출될 때마다 함수는 마지막으로 중지된 yield 문 다음에서부터 실행을 재개합니다. 다음 yield 문을 만날 때까지 실행되고, yield된 값이 반환된다.
  5. StopIteration 예외:
    • 제너레이터 함수가 모든 yield 문을 실행한 후 더 이상 반환할 값이 없으면, StopIteration 예외가 발생하여 반복이 종료된다.

제너레이터 함수의 특징

  1. 게으른 평가(Lazy Evaluation):
    • 제너레이터 함수는 필요할 때마다 값을 생성한다. 이를 통해 메모리 사용을 최소화할 수 있다. 이는 특히 큰 데이터셋을 처리할 때 유용하다.
  2. 상태 유지:
    • 제너레이터 함수는 상태를 유지한다. yield 문에서 중지된 후, 다음 호출에서 그 지점부터 다시 실행을 시작한다. 이를 통해 복잡한 상태 관리를 간단하게 처리할 수 있다.
  3. 이터레이터 프로토콜 준수:
    • 제너레이터 객체는 __iter__와 next 메서드를 구현하여 이터레이터 프로토콜을 따른다. 따라서 제너레이터 객체는 for 루프와 같은 반복문에서 사용할 수 있다.

예제: 간단한 제너레이터 함수

def simple_generator():
    yield 1
    yield 2
    yield 3

gen = simple_generator()
print(next(gen))  # 출력: 1
print(next(gen))  # 출력: 2
print(next(gen))  # 출력: 3
print(next(gen))  # StopIteration 예외 발생

예제: 피보나치 수열 제너레이터

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
for _ in range(10):
    print(next(fib))  # 첫 10개의 피보나치 수 출력

제너레이터 함수의 장점

  1. 메모리 효율성:
    • 제너레이터 함수는 한 번에 하나의 값만 생성하므로 메모리 사용량을 줄일 수 있습니다. 이는 큰 데이터셋을 처리할 때 특히 유용하다.
  2. 코드 간결성:
    • 제너레이터 함수를 사용하면 복잡한 이터레이터 객체를 직접 구현하지 않고도 쉽게 반복 가능한 객체를 만들 수 있다.
  3. 게으른 평가(Lazy Evaluation):
    • 제너레이터 함수는 필요할 때만 값을 계산하므로, 계산 비용이 높은 작업을 효율적으로 처리할 수 있다.

제너레이터 함수는 Python에서 매우 강력한 도구로, 다양한 상황에서 유용하게 사용할 수 있다. 이를 통해 메모리 효율성과 코드 간결성을 모두 확보할 수 있다.

 

WordIterator 느긋한 계산법(구현)

느긋한 계산법 (Lazy Evaluation)

느긋한 계산법(Lazy Evaluation)은 계산의 결과가 실제로 필요할 때까지 계산을 미루는 전략이다. 즉, 표현식의 값을 필요할 때만 계산하여 메모리 사용을 최적화하고 성능을 향상시킨다. 이는 특히 큰 데이터셋을 처리할 때 매우 유용하다.

Python에서는 제너레이터와 iter 함수가 느긋한 계산법을 구현하는 주요 도구이다. 제너레이터는 값을 하나씩 생성하며, 필요할 때만 값을 계산하므로 메모리 사용을 최소화할 수 있다.

re.finditer를 사용한 WordIterator

re.finditer는 re.findall과 달리 정규식 패턴에 매칭되는 모든 부분을 한꺼번에 리스트로 반환하지 않고, 매칭되는 객체를 하나씩 반환하는 이터레이터를 생성한다. 이는 메모리 효율성을 높이는 데 도움이 된다.

다음은 re.finditer를 사용하여 WordIterator를 구현한 코드이다:

import re

class WordIterable:
    def __init__(self, text):
        self.text = text

    def __iter__(self):
        for match in re.finditer(r'\\b\\w+\\b', self.text):
            yield match.group()

# Example usage
text = "This is a sample text with several words."
word_iterable = WordIterable(text)
for word in word_iterable:
    print(word)

코드 설명

  1. WordIterable 클래스:
    • init 메서드: 주어진 텍스트를 저장한다.
    • iter 메서드: word_generator 제너레이터 함수를 호출하여 제너레이터 객체를 반환한다.
    • word_generator 메서드: re.finditer를 사용하여 텍스트에서 단어를 찾아 매칭된 객체를 하나씩 반환한다. 각 매칭된 객체에서 단어를 추출하여 yield 한다.

장점

  1. 메모리 효율성:
    • re.finditer는 매칭된 객체를 하나씩 반환하므로, 전체 매칭 결과를 메모리에 저장하지 않다. 이는 매우 큰 텍스트를 처리할 때 메모리 사용량을 크게 줄일 수 있다.
  2. 성능 향상:
    • 필요한 부분만을 처리하여 성능을 최적화할 수 있다. 매칭된 결과를 하나씩 처리하기 때문에 초기 로드 시간이 감소할 수 있다.
  3. 게으른 계산(Lazy Evaluation):
    • 제너레이터 함수와 re.finditer를 사용하여 필요한 시점에만 값을 계산한다. 이는 전체 데이터를 미리 계산하지 않고, 필요할 때마다 데이터를 처리하는 효율적인 접근 방식이다.

이처럼 느긋한 계산법과 re.finditer를 사용하면 큰 데이터셋을 효과적으로 처리할 수 있으며, 메모리 사용량을 줄이고 성능을 최적화할 수 있다.

 

WordIterable (제네레이터 표현식)

제너레이터 표현식(Generator Expression)을 사용하면 코드가 더 간결해질 수 있다. 제너레이터 표현식은 리스트 컴프리헨션과 비슷하지만, 대괄호 대신 소괄호를 사용하며, 제너레이터 객체를 반환한다. 이를 통해 메모리 효율성을 높이고, 필요할 때마다 값을 계산할 수 있다.

다음은 WordIterable 클래스를 제너레이터 표현식을 사용하여 구현한 예제이다:

import re

class WordIterable:
    def __init__(self, text):
        self.text = text

    def __iter__(self):
        return (match.group() for match in re.finditer(r'\\b\\w+\\b', self.text))

# Example usage
text = "This is a sample text with several words."
word_iterable = WordIterable(text)
for word in word_iterable:
    print(word)

코드 설명

  1. WordIterable 클래스:
    • init 메서드: 주어진 텍스트를 저장한다.
    • iter 메서드: 제너레이터 표현식을 사용하여 텍스트에서 단어를 찾는다. re.finditer 함수는 매칭된 객체를 하나씩 반환하는 이터레이터를 생성한다. 제너레이터 표현식은 이 이터레이터에서 각 매칭된 객체의 단어를 추출하여 순차적으로 반환한다.

장점

  1. 간결함:
    • 제너레이터 표현식을 사용하면 코드가 매우 간결해진다. iter 메서드 내에 한 줄의 코드로 제너레이터를 정의할 수 있다.
  2. 메모리 효율성:
    • 제너레이터 표현식은 한 번에 하나의 값을 생성하므로 메모리 사용을 최소화한다. 이는 매우 큰 데이터셋을 처리할 때 특히 유용하다.
  3. 게으른 계산(Lazy Evaluation):
    • 제너레이터 표현식은 필요할 때마다 값을 계산한다. 이를 통해 불필요한 계산을 피하고 성능을 최적화할 수 있다.

이처럼 제너레이터 표현식을 사용하면 코드가 간결해지고, 메모리 효율성과 성능이 최적화된다. 이는 특히 큰 데이터셋을 처리할 때 유용하다.

 

itertools를 이용한 등차수열

itertools 모듈을 사용하여 등차수열을 생성할 수 있다. 등차수열은 연속된 항 사이의 차이가 일정한 수열을 말한다. itertools 모듈의 count 함수는 무한히 증가하는 수열을 생성하는데 유용하다.

다음은 itertools.count를 사용하여 등차수열을 생성하는 예제이다:

import itertools

# 등차수열 생성 함수
def arithmetic_sequence(start, step):
    return itertools.count(start, step)

# 예제 사용
start = 0  # 시작 값
step = 2   # 공차

sequence = arithmetic_sequence(start, step)

# 등차수열의 처음 10개 항 출력
for _ in range(10):
    print(next(sequence))
    
'''
0
2
4
6
8
10
12
14
16
18
'''

코드 설명

  1. arithmetic_sequence 함수:
    • itertools.count(start, step)를 사용하여 등차수열을 생성한다.
    • start는 시작 값을 나타내고, step은 공차를 나타낸다.
    • 이 함수는 무한히 증가하는 수열을 반환한다.
  2. 예제 사용:
    • start 값을 0으로 설정하고, step 값을 2로 설정하여 2씩 증가하는 등차수열을 생성한다.
    • for 루프를 사용하여 등차수열의 처음 10개 항을 출력한다.

장점

  1. 간결함:
    • itertools.count를 사용하면 간단한 코드로 무한 등차수열을 생성할 수 있다.
  2. 메모리 효율성:
    • 제너레이터를 사용하여 필요할 때마다 값을 생성하므로 메모리 사용을 최소화한다.
  3. 유연성:
    • start와 step 값을 조정하여 다양한 등차수열을 쉽게 생성할 수 있다.

이 예제에서는 0부터 시작하여 2씩 증가하는 등차수열의 처음 10개 항을 출력한다. itertools.count를 사용하면 무한 등차수열을 쉽게 생성할 수 있으며, 필요할 때마다 값을 계산하므로 메모리 효율성도 높다.

 

표준 라이브러리의 제너레이터 함수

Python 표준 라이브러리는 여러 가지 제너레이터 함수를 제공하여 다양한 반복 작업을 쉽게 수행할 수 있게 한다. 이 제너레이터 함수들은 주로 itertools 모듈과 collections 모듈에 포함되어 있다. 이 모듈들의 주요 제너레이터 함수와 그 용도에 대해 자세히 설명하겠다.

itertools 모듈의 제너레이터 함수

itertools 모듈은 반복 가능한 데이터 스트림을 효율적으로 처리하기 위한 다양한 함수들을 제공한다. 이 함수들은 주로 무한 반복, 조합 생성, 필터링 등의 작업에 사용된다.

  1. itertools.count(start=0, step=1):
    • 무한히 증가하는 수열을 생성한다.
    • start는 시작 값을, step은 증가 간격을 나타낸다.
    import itertools
    for num in itertools.count(10, 2):
        if num > 20:
            break
        print(num)  # 10, 12, 14, 16, 18, 20
    
  2. itertools.cycle(iterable):
    • 반복 가능한 객체의 요소들을 무한히 반복한다.
    import itertools
    for i, item in enumerate(itertools.cycle(['A', 'B', 'C'])):
        if i >= 9:
            break
        print(item)  # A B C A B C A B C
    
  3. itertools.repeat(object, times=None):
    • 주어진 객체를 무한히 또는 지정된 횟수만큼 반복한다.
    import itertools
    for item in itertools.repeat('Hello', 3):
        print(item)  # Hello Hello Hello
    
  4. itertools.chain(*iterables):
    • 여러 반복 가능한 객체를 연결하여 하나의 반복 가능한 객체로 만든다.
    import itertools
    for item in itertools.chain([1, 2, 3], ['a', 'b', 'c']):
        print(item)  # 1 2 3 a b c
    
  5. itertools.compress(data, selectors):
    • selectors의 요소가 참인 위치에 대응하는 data의 요소만을 반환한다.
    import itertools
    data = ['A', 'B', 'C', 'D']
    selectors = [1, 0, 1, 0]
    for item in itertools.compress(data, selectors):
        print(item)  # A C
    
  6. itertools.dropwhile(predicate, iterable):
    • 조건(predicate)이 참인 동안은 요소를 건너뛰고, 조건이 거짓이 되는 첫 번째 요소부터 반환한다.
    import itertools
    for item in itertools.dropwhile(lambda x: x < 5, [1, 4, 6, 4, 1]):
        print(item)  # 6 4 1
    
  7. itertools.takewhile(predicate, iterable):
    • 조건(predicate)이 참인 동안의 요소만 반환하고, 조건이 거짓이 되는 첫 번째 요소에서 중지한다.
    import itertools
    for item in itertools.takewhile(lambda x: x < 5, [1, 4, 6, 4, 1]):
        print(item)  # 1 4
    
  8. itertools.islice(iterable, start, stop, step):
    • 반복 가능한 객체의 슬라이스를 반환한다. start, stop, step 인덱스를 지정할 수 있다.
    import itertools
    for item in itertools.islice(range(10), 1, 8, 2):
        print(item)  # 1 3 5 7
    
  9. itertools.starmap(function, iterable):
    • 각 요소가 인수 튜플인 반복 가능한 객체에 대해 주어진 함수 function을 적용한다.
    import itertools
    for item in itertools.starmap(pow, [(2, 5), (3, 2), (10, 3)]):
        print(item)  # 32 9 1000
    
  10. itertools.product(*iterables, repeat=1):
    • 여러 반복 가능한 객체의 데카르트 곱을 반환한다.
    import itertools
    for item in itertools.product('AB', '12'):
        print(item)  # ('A', '1') ('A', '2') ('B', '1') ('B', '2')
    
  11. itertools.permutations(iterable, r=None):
    • 반복 가능한 객체의 요소들로 구성된 길이 r의 순열을 반환한다.
    import itertools
    for item in itertools.permutations('ABC', 2):
        print(item)  # ('A', 'B') ('A', 'C') ('B', 'A') ('B', 'C') ('C', 'A') ('C', 'B')
    
  12. itertools.combinations(iterable, r):
    • 반복 가능한 객체의 요소들로 구성된 길이 r의 조합을 반환한다.
    import itertools
    for item in itertools.combinations('ABC', 2):
        print(item)  # ('A', 'B') ('A', 'C') ('B', 'C')
    
  13. itertools.combinations_with_replacement(iterable, r):
    • 반복 가능한 객체의 요소들로 구성된 길이 r의 중복 조합을 반환한다.
    import itertools
    for item in itertools.combinations_with_replacement('ABC', 2):
        print(item)  # ('A', 'A') ('A', 'B') ('A', 'C') ('B', 'B') ('B', 'C') ('C', 'C')
    

collections 모듈의 제너레이터 함수

collections 모듈도 몇 가지 유용한 제너레이터 함수를 제공한다.

  1. collections.deque(iterable, maxlen):
    • deque는 양쪽 끝에서 빠르게 추가와 삭제가 가능한 큐이다. iterable로부터 초기 요소들을 받아온다. maxlen을 지정하면, 최대 길이가 고정된 덱을 생성한다. 이 경우 새로운 항목이 추가되면 가장 오래된 항목이 자동으로 삭제된다.
    from collections import deque
    d = deque(maxlen=3)
    for i in range(5):
        d.append(i)
        print(d)
    
  2. collections.defaultdict(default_factory):
    • defaultdict는 기본값을 제공하는 딕셔너리이다. default_factory 함수는 기본값을 생성한다. 이를 통해 키가 존재하지 않을 때 자동으로 기본값을 생성할 수 있다.
    from collections import defaultdict
    d = defaultdict(int)
    d['a'] += 1
    print(d)
    
  3. collections.OrderedDict:
    • 삽입 순서를 기억하는 딕셔너리입니다. 항목을 삽입한 순서대로 반복할 수 있다.
    from collections import OrderedDict
    d = OrderedDict()
    d['one'] = 1
    d['two'] = 2
    for key in d:
        print(key, d[key])
    
  4. collections.Counter:
    • 요소의 개수를 세는 딕셔너리 서브 클래스이다. 요소가 딕셔너리 키가 되고, 그 개수가 딕셔너리 값이 된다.
    from collections import Counter
    cnt = Counter('abracadabra')
    print(cnt)
    
  5. collections.ChainMap:
    • 여러 매핑 객체를 하나의 뷰로 연결한다. 여러 딕셔너리를 하나의 논리적 매핑 객체로 결합할 수 있다.
    from collections import ChainMap
    dict1 = {'one': 1, 'two': 2}
    dict2 = {'three': 3, 'four': 4}
    chain = ChainMap(dict1, dict2)
    print(chain)
    

장점

  1. 메모리 효율성:
    • 제너레이터 함수는 필요한 시점에 값을 생성하므로 메모리 사용량을 최소화할 수 있다. 이는 매우 큰 데이터셋을 처리할 때 특히 유용한다.
  2. 코드 간결성:
    • 제너레이터 함수를 사용하면 복잡한 이터레이터 객체를 직접 구현하지 않고도 쉽게 반복 가능한 객체를 만들 수 있다.
  3. 게으른 계산(Lazy Evaluation):
    • 제너레이터 함수는 필요할 때만 값을 계산하므로 불필요한 계산을 피하고 성능을 최적화할 수 있다.

이러한 제너레이터 함수들은 Python에서 매우 강력한 도구로, 다양한 상황에서 유용하게 사용할 수 있다. 이를 통해 메모리 효율성과 코드 간결성을 모두 확보할 수 있다.

 

yield from

yield from은 Python 3.3에서 도입된 문법으로, 제너레이터 함수 내에서 다른 제너레이터나 이터레이터를 간단히 위임(delegate)할 수 있는 기능을 제공한다. 이를 사용하면 서브 제너레이터(또는 서브 이터레이터)의 항목을 자동으로 반복하여 반환할 수 있다. yield from을 사용하면 코드가 더 간결해지고, 중첩된 반복 구조를 처리할 때 더 읽기 쉬워진다.

기본 사용법

yield from의 기본적인 사용법은 다음과 같다:

def generator1():
    yield from range(5)
    yield from "abc"

# Usage
for value in generator1():
    print(value)

'''
0
1
2
3
4
a
b
c
'''

이 코드는 range(5)와 "abc"의 각 항목을 하나씩 반환한다.

동작 원리

yield from <iterable> 구문은 다음과 같은 일련의 작업을 수행한다:

  1. 이터레이터 생성: <iterable>에서 이터레이터를 생성한다.
  2. 값 위임: 이터레이터의 각 값을 호출자의 제너레이터로 위임하여 yield 한다.
  3. 종료 처리: 이터레이터가 완료되면, 제어를 다시 제너레이터 본체로 되돌린다.

예제: 중첩된 제너레이터

다음은 중첩된 제너레이터 함수에서 yield from을 사용하는 예제이다:

def subgenerator():
    yield from range(3)

def delegating_generator():
    yield from subgenerator()
    yield from subgenerator()

# Usage
for value in delegating_generator():
    print(value)
    
'''
0
1
2
0
1
2
'''

이 예제에서 delegating_generator는 subgenerator의 값을 반복하여 두 번 반환한다.

장점

  1. 코드 간결성:
    • 중첩된 제너레이터를 사용하는 경우 yield from을 사용하면 코드를 더 간결하게 작성할 수 있다. 반복문을 사용하여 각 값을 수동으로 반환하는 것보다 훨씬 간단하다.
  2. 재사용성:
    • 제너레이터 함수를 작게 분리하고, 이를 조합하여 복잡한 반복 구조를 쉽게 만들 수 있다. 이는 코드의 모듈성을 높이고 재사용성을 향상시킨다.
  3. 예외 전파:
    • yield from은 제너레이터에 전달된 예외를 자동으로 서브 제너레이터에 전파한다. 이를 통해 예외 처리가 더욱 간단해진다.

예제: 예외 전파

yield from을 사용하면 예외도 전파된다.

def subgenerator():
    try:
        yield 1
        yield 2
    except ValueError:
        yield "ValueError caught"

def delegating_generator():
    yield from subgenerator()

# Usage
gen = delegating_generator()
print(next(gen))  # 1
print(next(gen))  # 2
gen.throw(ValueError)  # "ValueError caught"

'''
1
2
ValueError caught
'''

이 예제에서는 delegating_generator가 subgenerator에서 발생한 ValueError 예외를 처리한다.

yield from은 제너레이터를 다루는 데 있어 매우 유용한 도구로, 코드를 간결하고 명확하게 만들어준다. 중첩된 반복 구조를 처리하거나, 제너레이터 간의 위임을 쉽게 할 수 있게 해준다. 이를 통해 복잡한 반복 작업을 더 간단하고 효율적으로 구현할 수 있다.

 

iter() 함수

Python의 iter() 함수는 반복 가능한 객체(iterable)로부터 이터레이터(iterator)를 반환한다. 이터레이터는 iter()와 next() 메서드를 갖고 있으며, 이터러블 객체의 요소들을 순차적으로 반환할 수 있다. iter() 함수의 작동 방식과 이론적인 배경에 대해 자세히 설명하겠다.

이터러블과 이터레이터

  • 이터러블(Iterable): 이터러블 객체는 iter() 메서드를 구현하는 객체이다. 이 메서드는 이터레이터를 반환한다. 리스트, 튜플, 문자열, 사전, 집합 등이 이터러블 객체의 예이다.
  • 이터레이터(Iterator): 이터레이터 객체는 iter()와 next() 메서드를 구현하는 객체이다. iter() 메서드는 이터레이터 자신을 반환하고, next() 메서드는 다음 요소를 반환하며, 더 이상 반환할 요소가 없으면 StopIteration 예외를 발생시킨다.

iter() 함수의 작동 방식

iter() 함수는 두 가지 주요 시나리오에서 작동한다:

  1. 이터러블 객체로부터 이터레이터 생성:
    • iter() 함수는 이터러블 객체를 인수로 받아 iter() 메서드를 호출하여 이터레이터를 반환한다.
    my_list = [1, 2, 3]
    iterator = iter(my_list)  # list_iterator 객체 반환
    print(next(iterator))  # 출력: 1
    print(next(iterator))  # 출력: 2
    print(next(iterator))  # 출력: 3
    
  2. 반복 가능한 객체를 생성하는 함수로부터 이터레이터 생성:
    • 두 번째 형태로 iter(callable, sentinel)이 있습니다. 이 경우, callable 객체를 반복 호출하여 값을 생성한다. callable()이 sentinel 값을 반환하면 반복을 중지한다.
    import random
    
    def random_gen():
        return random.randint(0, 10)
    
    random_iter = iter(random_gen, 5)
    for num in random_iter:
        print(num)  # 5가 반환되면 반복 종료
    

이터레이터 프로토콜

이터레이터 프로토콜은 Python에서 반복을 지원하기 위한 표준이다. 이 프로토콜을 준수하면 사용자 정의 객체도 for 루프 등에서 사용할 수 있게 된다.

  1. iter() 메서드:
    • 이터레이터 객체 자신을 반환한다. 이는 이터레이터가 이터러블 객체로서 사용될 수 있게 한다.
    class MyIterator:
        def __init__(self, data):
            self.data = data
            self.index = 0
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.index < len(self.data):
                result = self.data[self.index]
                self.index += 1
                return result
            else:
                raise StopIteration
    
    my_iter = MyIterator([1, 2, 3])
    for item in my_iter:
        print(item)
    
  2. next() 메서드:
    • 이터레이터의 다음 요소를 반환한다. 더 이상 반환할 요소가 없으면 StopIteration 예외를 발생시켜 반복을 종료한다.

iter() 함수의 예제

  • 이터러블 객체 예제:
my_list = [1, 2, 3]
iterator = iter(my_list)
print(next(iterator))  # 출력: 1
print(next(iterator))  # 출력: 2
print(next(iterator))  # 출력: 3
  • 반복 가능한 객체 생성 함수 예제: 
import random

def random_gen():
    return random.randint(0, 10)

random_iter = iter(random_gen, 5)
for num in random_iter:
    print(num)  # 5가 반환되면 반복 종료

장점

  1. 메모리 효율성:
    • 이터레이터는 필요할 때마다 값을 생성하므로 메모리 사용을 최소화할 수 있다. 이는 특히 큰 데이터셋을 처리할 때 유용한다.
  2. 코드 간결성:
    • 이터레이터를 사용하면 복잡한 반복 작업을 간단하게 구현할 수 있다. for 루프와 함께 사용하면 코드가 더 간결하고 명확해진다.
  3. 지연 평가(Lazy Evaluation):
    • 이터레이터는 필요할 때만 값을 계산하므로 성능을 최적화할 수 있다. 불필요한 계산을 피하고, 실제로 필요할 때만 데이터를 생성한다.

iter() 함수는 Python의 반복 프로토콜을 지원하는 중요한 함수이다. 이터러블 객체로부터 이터레이터를 생성하거나, 반복 가능한 객체를 생성하는 함수로부터 이터레이터를 생성할 수 있다. 이를 통해 메모리 효율성과 코드 간결성을 높일 수 있으며, 큰 데이터셋을 효과적으로 처리할 수 있다.

 

코루틴 (Coroutine)

코루틴(Coroutine)은 비동기 프로그래밍을 위해 설계된 특수한 형태의 함수이다. 일반적인 함수는 호출되면 시작부터 끝까지 한 번에 실행되지만, 코루틴은 실행을 중단하고 다른 코루틴이나 함수를 실행한 다음, 나중에 중단된 지점부터 다시 실행을 재개할 수 있다. 이를 통해 효율적인 비동기 I/O 작업, 이벤트 루프, 협력적 멀티태스킹 등을 구현할 수 있다.

코루틴의 특징

  1. 중단과 재개:
    • yield나 await 키워드를 사용하여 실행을 중단하고, 다시 재개할 수 있다.
  2. 비동기 처리:
    • I/O 작업이나 시간이 오래 걸리는 작업을 수행할 때 다른 작업을 블록킹하지 않고 비동기적으로 처리할 수 있다.
  3. 상태 유지:
    • 중단된 지점의 상태를 유지하여, 이후 재개될 때 상태를 이어서 작업할 수 있다.

코루틴의 기본 사용법

Python에서는 async def로 코루틴을 정의하고, await 키워드로 다른 코루틴의 완료를 기다릴 수 있다.

import asyncio

async def my_coroutine():
    print("Hello")
    await asyncio.sleep(1)  # 비동기적으로 1초 대기
    print("World")

# 코루틴 실행
asyncio.run(my_coroutine())

이 예제에서 my_coroutine 함수는 코루틴으로 정의되었으며, await asyncio.sleep(1)을 통해 1초 동안 비동기적으로 대기한다. asyncio.run을 사용하여 코루틴을 실행한다.

코루틴과 제너레이터의 차이점

  • 제너레이터:
    • yield 키워드를 사용하여 값을 반환하고, 중단된 지점에서 다시 시작할 수 있다.
    • 주로 데이터 스트림을 생성하거나 반복 작업을 수행할 때 사용된다.
  • 코루틴:
    • await 키워드를 사용하여 다른 비동기 작업을 기다릴 수 있다.
    • 비동기 I/O 작업이나 이벤트 기반 프로그래밍에 주로 사용된다.

코루틴의 장점

  1. 비동기 I/O 처리:
    • 네트워크 요청, 파일 읽기/쓰기 등 I/O 작업을 비동기적으로 처리하여 응답성을 높일 수 있다.
  2. 효율적인 멀티태스킹:
    • 협력적 멀티태스킹을 통해 여러 작업을 효율적으로 수행할 수 있다.
  3. 상태 유지:
    • 중단된 지점의 상태를 유지하여 복잡한 작업 흐름을 쉽게 관리할 수 있다.

코루틴은 비동기 프로그래밍에서 중요한 역할을 한다. 코루틴을 사용하면 비동기 작업을 쉽게 구현하고, I/O 바운드 작업의 성능을 향상시킬 수 있다. Python에서는 async와 await 키워드를 통해 코루틴을 쉽게 정의하고 사용할 수 있다.

 

모든 코드는 github에 저장되어 있습니다.

https://github.com/SeongUk18/python

728x90

'Python study' 카테고리의 다른 글

코루틴 (coroutine)  (1) 2024.06.04
콘텍스트 관리자와 else 블록  (0) 2024.06.04
연산자 오버로딩  (0) 2024.05.29
다중 상속 (Multiple inheritance)  (0) 2024.05.28
인터페이스  (0) 2024.05.26