본문 바로가기

Python/Python

[Python] yield를 사용하는 제네레이터와 itertools의 다양한 함수들

반응형

이번 포스팅에서는 yield 키워드를 사용하는 제네레이터 함수를 만들어보고 이것이 어떻게 병행성을 구현할 수 있는지 생각해보자. 그리고 itertools 라이브러리의 다양한 함수들에 대해 간략히 사용법에 대해 알아보자.

 


1. yield를 사용한 제네레이터

보통 함수를 작성할 때 우리는 return 을 사용해왔다. 그러나 return 대신 yield 키워드를 사용하게 되면 제네레이터 함수를 만들어주어 병행성을 구현할 수 있다. 여기서 병행성이란, 하나의 프로세서 또는 쓰레드가 한 번에 여러가지 일을 순차적으로 번갈아가면서 수행해주어 사람의 눈에서는 여러개의 일을 동시에 처리하는 것처럼 보이게 되는 것을 말한다. 참고로 병렬성은 정말로 한 번에 여러가지 일을 실행하는 것을 의미한다.

 

그렇다면 파이썬에서 병행성을 어떻게 구현할 수 있을까? 추후에 배울 코루틴으로 이어지겠지만, 코루틴을 배우기 위해서는 제네레이터를 알아야 하고 바로 이 제네레이터가 병행성을 구현하는 실질적인 요소라고 할 수 있다. 우선 하나의 상황을 가정해보자. 우리는 현재 3가지의 업무를 병행성으로 실행하려 한다. 그 3가지 일은 바로 '구글 페이지에서 크롤링하는 것', '네이버 페이지에서 크롤링하는 것', '다음 페이지에서 크롤링하는 것'. 이 때 구글 페이지에서 크롤링하는 것을 수행한 후 잠깐 다른 것을 급하게 처리할 일이 있어서 다른 함수를 호출해야 하는 상황이다. 만약 일반적으로 함수에 return을 사용하게 되면 우리는 급하게 처리할 다른 것을 도중에 수행하지 못하고 3가지 페이지들을 모두 크롤링이 완료될 때까지 기다려야 한다. 바로 아래와 같이 작성하면 말이다.

 

def crawling_function():
    crawl_google_page()
    
    crawl_naver_page()
    
    crawl_daum_page()
    
    return

 

그런데 yield를 사용해서 아래와 같이 작성하게 되면 우리는 위 3가지 페이지를 크롤링하는 것을 단계적으로 쪼개어 병행성을 구현할 수 있다. 

 

def crawling_generator():
    yield crawl_google_page()
    
    # 이후 급하게 처리할 다른 것을 수행할 때까지 대기 상태
    yield crawl_naver_page()

    yield crawl_daum_page()

 

위처럼 만든 제네레이터 함수를 단계적으로 하나씩 호출하는 것은 next() 매직 메소드를 활용하면 된다. 아래 처럼 말이다.

 

def crawling_generator():
    yield crawl_google_page()

    yield crawl_naver_page()

    yield crawl_daum_page()

crawling_gen = crawling_generator()
# next 1번 호출함으로써 구글 페이지 크롤링 수행 후 대기!
next(crawling_gen)
# 다음 next 호출함으로써 네이버 페이지 크롤링 수행 후 대기!
next(crawling_gen)
# 마지막 next 호출함으로써 다음 페이지 크롤링 수행 후 대기!
next(crawling_gen)

 

이것은 기타 내용인데, 보통 list comprehension 으로 우리는 list를 편하게 만들어준다. 근데 이런 컴프리헨션 문법을 활용해서 제네레이터도 손쉽게 만들 수 있다. 바로 괄호의 모양만 [ ] -> ( ) 로 바꾸어주면 된다.

 

# 소괄호 형태의 컴프리헨션 문법으로 제네레이터를 생성 가능
temp1 = [str(x) * 3 for x in range(0, 10)]
temp2 = (str(x) * 3 for x in range(0, 10))
print('temp1:', temp1)
print('temp2:', temp2) # generator 객체
for v in temp2:
    print(v)

2. itertools 라이브러리의 다양한 함수들

저번 포스팅에서 알고리즘 문제를 풀 때 자주 사용되는 itertools 라이브러리의 함수들에 대해서도 알아보았다. 이번에는 그 때 살펴보지 못했던 다른 함수들을 살펴보자.

2-1. itertools.count

이 함수는 start, step 을 인자로 넣어줄 수 있는데, start로 설정된 숫자부터 양의 무한대까지 step 단위로 숫자를 출력해준다. 마찬가지로 제네레이터 같은 형태로 반환되어서 next 매직메소드나 for loop를 활용해서 출력해줄 수 있다.

 

import itertools

count_gen = itertools.count(1, 2.5)

print(next(count_gen)) # 제네레이터처럼 next를 호출해서 원소를 반환 가능

2-2. itertools.takewhile

이 함수는 특정 조건에 해당하는 값들만 필터링해서 제네레이터를 생성한다. 인자로는 조건을 명시할 함수, iterable한 객체를 넣어준다.

 

import itertools

takewhile_gen = itertools.takewhile(lambda x : x < 10, itertools.count(0, 1))
for v in takewhile_gen:
    print(v)

2-3. itertools.filterfalse

이 함수는 특정 조건을 만족하지 않는 값들만 필터링해서 제네레이터를 생성한다. 인자로는 조건을 명시할 함수, iterable한 객체를 넣어준다.

 

import itertools

# 참고로 iterable한 객체 인자에 itertools.count 넣어주면 for loop를 돌았을 때 종단되지 않음..
filterfalse_gen = itertools.filterfalse(lambda x : x > 20, [x for x in range(0, 40, 2)])
for v in filterfalse_gen:
    print(v)

2-4. itertools.accumulate

이 함수는 이름에도 알 수 있다 시피 iterable한 객체를 넣어주면 인덱스가 증가하는 순서대로 누적합을 만들어준다.

 

import itertools

accumulate_gen = itertools.accumulate([x for x in range(1, 10)])
for v in accumulate_gen:
    print(v)

2-5. itertools.chain

이 함수는 두 자료구조를 연결해준 리스트를 반환해준다. 이 때, 두 자료구조를 모두 튜플 또는 모두 리스트 또는 튜플, 리스트 두 개를 섞어서 넣어주어도 함수가 반환해주는 자료구조는 무조건 리스트이다.

 

import itertools

chain_gen1 = itertools.chain('abcde', [str(x) for x in range(0 ,20)])
print('chain_gen1:', list(chain_gen1))
chain_gen2 = itertools.chain(('a','b','c'), [str(x) for x in range(0, 10)]) # tuple & list 연결도 가능
print('chain_gen2:', list(chain_gen2))
chain_gen3 = itertools.chain(('a', 'b', 'c'), (str(1), str(2), str(3)))
print('chain_gen3:', list(chain_gen3))
chain_gen4 = itertools.chain(enumerate('ABCDE'))
print('chain_gen4:', list(chain_gen4))
chain_gen5 = itertools.chain(enumerate(range(0, 10)))
print('chain_gen5:', list(chain_gen5))
chain_gen6 = itertools.chain(enumerate([str(x) for x in range(0, 5)]))
print('chain_gen6:', list(chain_gen6))

2-6. itertools.groupby

이 함수는 인자로 넣어준 iterable한 객체의 각 원소끼리 똑같은 값들끼리 그룹핑을 해준다. 주의할 점은 groupby를 사용해 반환된 객체를 for loop로 호출해주어야 하는데, 이 때 똑같은 값들끼리 나열해주는 객체에 대해서는 list를 감싸주어야 한다. 아래와 같이 말이다.

 

import itertools

groupby_gen = itertools.groupby('aaabcddeeffsgggsera')
for unique_chr, chrs in groupby_gen:
    print(unique_chr, '->', list(chrs))
# a -> ['a', 'a', 'a']
# b -> ['b']
# c -> ['c']
# d -> ['d', 'd']
# e -> ['e', 'e']
# f -> ['f', 'f']
# s -> ['s']
# g -> ['g', 'g', 'g']
# s -> ['s']
# e -> ['e']
# r -> ['r']
# a -> ['a']
반응형