Iterator & generator

About_work·2023년 1월 8일
0

python 기초

목록 보기
4/65

1. iterator

Elements = [3, 4, 5]
For element in elements:
    Print(element)
  • 위 예시를 설명하자면,

    • for 루프에서 iterable object(Elements)iter(elements)를 호출하여 (해당 iterable의 __iter__() 메서드를 호출하여) iterator을 가져온 후,
    • 그 iterator의 next() 메서드를 호출하여 루프를 돌게 된다.
  • iterable 객체

    • for 문을 써서 하나씩 데이터를 처리할 수 있는 컬렉션이나 sequence 등
    • 어떤 클래스를 iterable 하게 하려면, 그 클래스의 iterator을 리턴하는 __iter__() 메서드를 작성해야 한다.
    • __iter__() 메서드
      • 리턴하는 iterator은 동일한 class 객체가 될 수도 있고,
      • 별도로 작성된 iterator 클래스의 객체가 될 수도 있다.
      • 반드시 iterator 를 return 해야 한다.(아니면 반드시 해당 함수를 generator 함수로 만들어야 한다)
  • iterator

    • iterable 객체가 __iter__ 메서드 를 실행한 return 값
    • iterable 객체에서 실제 iteation을 실행하는 주체
    • iterator은 iterator_object.__next__() method를 사용하여 다음 element를 가져온다.
    • 단약 더 이상 next 요소가 없으면, StopIteration Exception 을 발생시킨다.
    • iterator가 되는 클래스는 __next()__ 메서드를 구현해야 한다.
  • 내장 함수 iter()

    • iter( iterable object) 와 같이 사용
    • iterable object의 iterator을 리턴
  • 내장 함수 next()

    • next (iterator object) 와 같이 사용
My_list = [1, 2, 3]
It = iter(my_list)
>>> next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback ~~~
StopIteration
  • 직접 만드는 iterator 객체
    • __iter__() 메서드에서 self 를 return 함으로써 iterable 과 동일한 클래스에 iterator을 구현하였음을 표시
Class MyCollection:
    Def __init__(self):
        Self.size = 10
        Self.data = list(range(self.size))

    Def __iter__(self):
        Self.index = 0
        Return self

    Def __next__(self):
        If self.index >= self.size:
            Raise StopIteration
        N = self.data[self.index]
        Self.index += 1
        Return N

Coll.= Mycollection()
For x in coll:

    Print(x)

2. Generator

  • Iterator의 특수한 한 형태
  • generator 함수
    • 함수 안에 yield 를 사용하여, data를 하나씩 return 하는 함수
    • 함수가 처음 호출되면(iterator가 next 내장 함수를 호출하면), 그 함수 실행 중 처음으로 만나는 yield 에서 값을 return 한다.
    • 함수가 또 호출 되면(iterator가 next 내장 함수를 호출하면), 직전에 실행되었던 yield 문 다음부터 ~ 다음 yield 문을 만날 때까지 문장들을 실행하게 된다.
    • 이러한 generator 함수를 변수에 할당하면, 그 변수는 generator 클래스 객체가 된다.
    • 만들어진 generator 클래스 객체는 iterator 로써, next() 내장함수를 사용하여 generator의 값을 계속 가져올 수 있다.
    • generator 함수가 반환하는 iterator를, list() 로 감싸면, geneartor을 쉽게 리스트로 변환할 수 있다.
# generator 함수
def gen():
    yield 1
    yield 2
    yield 3

# generator 클래스 객체
g_class = gen()
print(type(g_class)) # <class ‘generator’>

# next () 함수 이용
n = next(g_class); # 1
n = next(g_class); # 2

# for 루프 이용 가능
for x in gen():
    print(x)

2.1. 제너레이터가 close() 메서드를 통해 종료되는 경우

  • 제너레이터의 close() 메서드는 제너레이터를 명시적으로 종료시키고자 할 때 사용
  • 이 메서드를 호출하면, 제너레이터 내에서 GeneratorExit 예외가 발생하며, 이를 통해 제너레이터가 자원을 해제하거나 필요한 종료 처리를 수행할 수 있음
  1. 자원의 명시적 해제:
  • 제너레이터가 파일이나 네트워크 연결과 같은 외부 자원을 사용하고 있을 때,
  • 제너레이터를 통해 더 이상 데이터를 생성할 필요가 없다면 close() 메서드를 호출하여 자원을 명시적으로 해제할 수 있음
  1. 제너레이터 사용 완료:
  • 제너레이터를 사용하여 필요한 데이터를 모두 처리했으나,
  • 제너레이터 함수의 실행이 자연스럽게 끝나지 않은 상태(예: 무한 루프 제너레이터)에서 사용을 완료했음을 명시적으로 알리고자 할 때
  • close() 메서드를 사용할 수 있습니다.
  1. 예외 처리:
  • 제너레이터 내부에서 발생할 수 있는 예외 상황을 외부에서 제어하고자 할 때,
  • close() 메서드를 호출하여 GeneratorExit 예외를 발생시키고 이를 처리함으로써 제너레이터의 실행을 안전하게 종료시킬 수 있습니다.

2.1.1. 사용 예:

def my_generator():
    try:
        yield 'Hello'
        yield 'World'
    except GeneratorExit:
        # 제너레이터 종료 시 필요한 정리 작업
        print('Generator is closing')

gen = my_generator()
print(next(gen))  # Hello 출력
print(next(gen))  # World 출력
gen.close()       # Generator is closing 출력

심화

generator와 일반 iterator의 차이

  • 리스트나 Set() 과 같은 컬렉션에 대한 iterator은 해당 컬렉션이 이미 모든 값을 가지고 있는 경우
  • generator은 모든 데이터를 미리 가지고 있지 않은 상태에서, yield 에 의해 하나씩만 데이터를 만들어 가져옴
  • generator은 언제쓰나?
    • 데이터가 무제한이어서 모든 data를 return 할 수 없는 경우
    • 데이터가 대량이어서 일부씩 처리하는 것이 필요한 경우
    • 모든 데이터를 미리 계산하면 속도가 느려서 그때 그때 On demand로 처리하는 것이 좋은 경우

generator expression

  • generator은 2가지 방식으로 만들 수 있다.
    • yield가 포함된 함수를 호출하기
    • generator comprehension 을 이용하여 만들기
  • comprehension 을 tuple 에 적용하려고 하면, 의도치 않게 generator 이 만들어진다.
  • 이를 generator comprehension 혹은 generator expression 이라고 부른다.
g = (n**2 for n in range(1001) ) # generator 객체 생성

for i in range(5):
    value = next(g)
    print(value)

1, 4, 9, 16, 25

for x in g:
    print(x)

36, 49, ..

원칙

list 를 return 하기보다는, generator을 사용하라.

  • generator을 사용하면, 결과를 list에 합쳐서 return 하는 함수보다 더 깔끔하다.
  • generator을 사용하면, 메모리에 모든 입력과 출력을 저장할 필요가 없으므로, 입력이 아주 커도 출력 sequence를 만들 수 있다.

함수의 argument를 iteration 할 때는 방어적이 되라.

  • 입력 받은 argument가 컨테이너가 아니라, iterator이면 함수가 이상하게 작동하거나, 결과가 없을 수 있다.
  • __iter__ 메서드를 generator로 정의하면, 쉽게 iterable container type을 정의할 수 있다.
class ReadVisits:
    def __init__(self, data_path):
        self.data_path = data_path

    def __iter__(self):
        with open(self.data_path) as f:
            for line in f:
                yield int(line)

visits = ReadVisits(path)
for line in visits:
	print(line)
  • 함수 내 어떤 값(argument)이 (컨테이너가 아닌) iterator 인지 감지하여 에러를 띄우고 싶다면,
    • iter(값) = 값 인지 확인하라.
    • collections.abc.Iterator 클래스를 이용해, if isinstance(값, Iterator) 로 확인하라.
def normalize_defensive(numbers):
    if iter(numbers) is numbers: # 이터레이터 -- 나쁨!
        raise TypeError('컨테이너를 제공해야 합니다')
    if isinstance(numbers, Iterator): # 반복 가능한 이터레이터인지 검사하는 다른 방법
        raise TypeError('컨테이너를 제공해야 합니다')
    total = sum(numbers)
    result = []
...
    return result

yield from 을 사용해, 여러 generator을 합성하라.

  • 잘못된 예시
def move(period, speed):
    for _ in range(period):
        yield speed

def animate():
    for delta in move(4, 5.0):
        yield delta
    for delta in pause(3):
        yield delta
    for delta in move(2, 3.0):
        yield delta
  • 아래와 같이 바꿔라
def animate_composed():
    yield from move(4, 5.0)
    yield from pause(3)
    yield from move(2, 3.0)
  • yield from 식을 사용하면, 여러 내장 generator을 하나로 합성할 수 있다.
  • 내포된 generator을 직접 iteration 하면서 각 generator의 출력을 내보내는 것보다,
    • yield from 을 사용하는 것이 성능면에서 더 좋다.
  • yield from은 제어를 부모 제네레이터에게 전달하기 전에, 내포된 generator가 모든 값을 내보낸다.

send로 generator에 데이터를 주입하지 마라.

  • yield는 단방향인데, 양방향 통신을 하고 싶다면, generator의 입력으로 iterator을 전달하라.
  • 합성할 generator들의 입력으로 iterator을 전달하는 것이, send를 이용하는 것보다 낫다. (send는 가급적 사용하지 말라)

def run_cascading():
    amplitudes = [7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10]
    it = complex_wave_cascading(iter(amplitudes))
    for amplitude in amplitudes:
        output = next(it)
        transmit(output)

run_cascading()

def complex_wave_cascading(amplitude_it):
    yield from wave_cascading(amplitude_it, 3)
    yield from wave_cascading(amplitude_it, 4)
    yield from wave_cascading(amplitude_it, 5)

def wave_cascading(amplitude_it, steps):
    step_size = 2 * math.pi / steps
    for step in range(steps):
        radians = step * step_size
        fraction = math.sin(radians)
        amplitude = next(amplitude_it) # 다음 입력 받기
        output = amplitude * fraction
        yield output

generator 안에서 throw로 상태를 변화시키지 말라.

  • generator에서 예외적인 동작 제공하기! (throw 는 쓰지마!)
  • generator에서 예외적인 동작을 제공하는 더 나은 방법은, __iter__ 메서드를 구현하는 클래스를 사용하면서 예외적인 경우에 상태를 transition 하는 것이다.

def run():
    timer = Timer(4)
    for current in timer:
        if check_for_reset():
            timer.reset()
        announce(current)

run()

#
class Timer:
    def __init__(self, period):
        self.current = period
        self.period = period

    def reset(self):
        self.current = self.period

    def __iter__(self):
        while self.current:
            self.current -= 1
            yield self.current

#
RESETS = [
    False, False, False, True, False, True, False,
    False, False, False, False, False, False, False]

def check_for_reset():
    # 외부 이벤트를 폴링한다
    return RESETS.pop(0)

>
3틱 남음
2틱 남음
1틱 남음
3틱 남음
2틱 남음
3틱 남음
2틱 남음
1틱 남음
0틱 남음

Iterator나 generator을 다룰 때는 itertools를 사용하라.

  • itertools에서 제공하는 함수는 세 가지 범주로 나눌 수 있다.

여러 iterator을 연결함

  • chain
import itertools

it = itertools.chain([1, 2, 3], [4, 5, 6])
print(list(it))
  • repeat
it = itertools.repeat('안녕', 3) # 3은 최대 반복 횟수
print(list(it))
  • cycle
it = itertools.cycle([1, 2])
result = [next(it) for _ in range (10)]
print(result)

>
1, 2, 1, 2, 1, 2, 1, 2, 1, 2 
  • tee ( 같은 iterator 여러개 만들기)
it1, it2, it3 = itertools.tee(['하나', '둘'], 3)
print(list(it1))
print(list(it2))
print(list(it3))
  • zip_longest
keys = ['하나', '둘', '셋']
values = [1, 2]

normal = list(zip(keys, values))
print('zip:', normal)

it = itertools.zip_longest(keys, values, fillvalue='없음')
# fillvalue 의 default는 None이다. 
longest = list(it)
print('zip_longest:', longest)

>
(‘하나’, 1)
(‘둘‘, 2)
(‘셋’, ‘없음’)

iterator의 원소를 걸러냄

  • islice (iterator을 복사하지 않으면서 슬라이싱 하고 싶을 때)

values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

first_five = itertools.islice(values, 5)
print('앞에서 다섯 개:', list(first_five))
>
[1, 2, 3, 4, 5]

middle_odds = itertools.islice(values, 2, 8, 2)
print('중간의 홀수들:', list(middle_odds))
>
[3, 5, 7]
  • takewhile (True를 반환하는 동안 원소를 돌려준다.)
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
less_than_seven = lambda x: x < 7
it = itertools.takewhile(less_than_seven, values)
print(list(it))

>>>
[1, 2, 3, 4, 5, 6]
  • dropwhile (True를 반환하는 동안 원소를 건너뛴다.)

values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
less_than_seven = lambda x: x < 7
it = itertools.dropwhile(less_than_seven, values)
print(list(it))

>>>
[7, 8, 9 ,10]
  • filterfalse (filter 함수와 반대)

values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = lambda x: x % 2 == 0

filter_result = filter(evens, values)
print('Filter:', list(filter_result))

>>>
Filter: [2, 4, 6, 8, 10]

filter_false_result = itertools.filterfalse(evens, values)
print('Filter false:', list(filter_false_result))

>>>
Filter false: [1, 3, 5, 7, 9]

원소의 조합을 만들어냄

  • accumulate
    • 파라미터를 2개 받는 함수를 반복 적용하면서, iterator 원소를 값 하나로 줄여준다.
    • 이 함수가 돌려주는 iterator은 원본 iterator의 각 원소에 대해 누적된 결과를 내놓는다.

values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
sum_reduce = itertools.accumulate(values)
print('합계:', list(sum_reduce))

>>>
합계: [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]

def sum_modulo_20(first, second):
    output = first + second
    return output % 20

modulo_reduce = itertools.accumulate(values, sum_modulo_20)
print('20으로 나눈 나머지의 합계:', list(modulo_reduce))

>>>
20으로 나눈 나머지의 합계: [ 1, 3, 6, 10, 15, 1, 8, 16, 5, 15]
  • product
    • list comprehension을 깊이 내포시키는 대신, 이 함수를 사용하면 편리하다.
    • 좋은기능!!!!!

single = itertools.product([1, 2], repeat=2)
print('리스트 한 개:', list(single))

>>>
리스트 한 개: [(1, 1), (1, 2), (2, 1), (2, 2)]

multiple = itertools.product([1, 2], ['a', 'b'])
print('리스트 두 개:', list(multiple))

리스트 두 개: [(1, ‘a’), (1, ‘b’), (2, ‘a’), (2, ‘b’)]
  • permutations
    • 좋은 기능! iterator가 내놓는 원소들로부터 만들어낸 길이 N인 순열(순서 있음)을 돌려준다.

it = itertools.permutations([1, 2, 3, 4], 2)
print(list(it))

>>>
[(1, 2), (1, 3), (1, 4), (2, 1), (2, 3), (2, 4), (3, 1), (3, 2), (3, 4), (4,1), (4, 2), (4, 3)]
  • combinations
    • 좋은 기능! iterator가 내놓는 원소들로부터 만들어낸 길이 N인 조합(순서 없음)을 돌려준다.

it = itertools.combinations([1, 2, 3, 4], 2)
print(list(it))

>>>
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
  • combinations_with_replacement
    • combinations과 같지만, 원소의 반복을 허용한다. (즉, 중복 조합을 돌려준다.)

it = itertools.combinations_with_replacement([1, 2, 3, 4], 2)
print(list(it))

>>>
[(1, 1), (1, 2), (1, 3), (1, 4), (2, 2), (2, 3), (2, 4), (3, 3), (3, 4), (4, 4)]
profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글