[Python] 파이썬 동시성 - iterable의 이해

728x90

파이썬에서는 병행성과 병렬성을 지원합니다. 

여기서 병행성이란 한 컴퓨터가 여러일을 동시에 수행하는 것을 의미합니다. 즉 하나의 프로세스에서 여러일을 쉽게 해결하는 것입니다. 

반면에 병렬성이란 여러 프로세스가 여러작업을 동시에 수행하는 것을 의미합니다. 예컨대 파이썬으로 크롤링을 할때 여러사이트에 동시에 접속해서 각각 정보를 수집하는 것 등입니다. 이러한 병렬성은 일반적으로 속도상승에 목적을 두고 있습니다.

 

파이썬에서 병행성을 이해하기 위해서는 제너레이터를 알아야합니다.

 

제너레이터의 개념은 아래 페이지를 참고하시면 됩니다.

https://zero-week.tistory.com/151

 

파이썬 시퀀스

1. 시퀀스파이썬에서 시퀀스는 여러 요소를 순서대로 나열한 데이터 구조를 의미합니다. 이러한 시퀀스는 저장하는 방식에 따라 컨테이너와 플랫으로 나눌 수 있고, 변경가능여부에 따라 가변

zero-week.tistory.com

 

 

 

또한 반복가능한 객체에 대한 이해도 필요합니다.

1. iter의 이해

1) 반복가능한 객체의 내부동작 원리

파이썬에서 반복가능한(iterable) 타입에는 여러가지가 있지만 문자열로 예제를 만들어보겠습니다.

txt = "abcd"

for c in txt:
    print("for문:",c)

#출력:
# for문: a
# for문: b
# for문: c
# for문: d

일반적으로 문자열을 반복할때는 위 코드처럼 표시합니다.

 

이것은 문자열이 반복가능한 타입이기 때문에 가능한 코드입니다. 이 코드는 내부적으로는 아래처럼 실행됩니다.

w1 = iter(txt)
while True:
    try:
        print("while문:",next(w1))
    except StopIteration:
        break
        
#출력: -> iter 가능한 객체가 내부 동작하는 원리
# while문: a
# while문: b
# while문: c
# while문: d

 

2) 반복 가능한지 확인하는 방법

객체가 반복가능한지 확인하는 방법에는 아래 3가지 방법이 있습니다.

print(dir(txt)) #-> __iter__를 찾는다 -> 있으면 반복 가능
print(hasattr(txt,'__iter__')) # True
print(isinstance(txt,abc.Iterable)) # True

 

 

3) 반복가능한 객체의 __next__메서드와  __iter__ 메서드 사용 비교

# next 패턴
class WordSplitter:
    def __init__(self,txt):
        self._idx = 0
        self._txt = txt.split(' ')

    def __next__(self):
        print("next 메서드 호출")
        try:
            word = self._txt[self._idx]
        except IndexError:
            raise StopIteration("반복 중단.")

        self._idx += 1
        return word

    def __repr__(self):
        return f'WordSplit({self._txt})'


w1 = WordSplitter("오늘 날씨는 맑습니다.")
print(w1) #출력: WordSplit(['오늘', '날씨는', '맑습니다.'])

print(next(w1))
#출력:
# next 메서드 호출
# 오늘

print(next(w1))
#출력:
# next 메서드 호출
# 날씨는

print(next(w1))
#출력:
# next 메서드 호출
# 맑습니다.

# 제너레이터 패턴
# 1. 지능형 리스트, 딕셔너리, 집합 생성 후 데이터 양이 증가할때 메모리 사용량도 증가 -> 제너레이터 사용권장(메모리 절약)
# 2. 단위 실행 가능한 코루틴 구현과 연동 가능
# 3. 작은 메모리 조각을 사용

class WordSplitterGenerator:
    def __init__(self,txt):
        self._txt = txt.split(' ')

    def __iter__(self):
        print("__iter__ 메서드 호출")
        for word in self._txt:
            yield word # 제너레이터

    def __repr__(self):
        return f'WordSplitGenerator({self._txt})'


wg1 = WordSplitterGenerator("오늘 날씨는 맑습니다.")

wt = iter(wg1)
print(wt) #출력: <generator object WordSplitterGenerator.__iter__ at 0x000001DF1C268B30>
print(wg1) #출력: WordSplitGenerator(['오늘', '날씨는', '맑습니다.'])

print(next(wt))
#출력: 
# __iter__ 메서드 호출
# 오늘
print(next(wt)) #출력: 날씨는
print(next(wt)) #출력: 맑습니다.

 

2. iter에서의 제너레이터

yield 는 제너레이터에서 사용되며 return의 역할을 합니다. 그러나 return과 다른점은 함수에서 return을 사용하면 함수가 종료되기 때문에 return을 두개 이상 사용할 수 없습니다. 또한 return에서 반복가능한 객체를 반환하면 첫번째 값만 반환됩니다. yield 는 이와 다르게 하나의 함수내에서 여러번 사용 가능하며 반복가능한 객체 전체를 반환합니다.

 

예제)

def return_test():
    print("aa")
    return "bcded" # b만 출력됨
    print("cc") # 출력안됨
    return "dd" # 출력안됨

rt = iter(return_test())
print(next(rt))
#출력:
# aa
# b

def generator_ex1():
    print("start")
    yield "A point" # return의 역할 -> 크롤링 함수를 넣는것도 가능
    print("continue")
    yield "B point"
    print("end")

temp = iter(generator_ex1())

print(next(temp))
#출력:
# start
# A point

print(next(temp))
#출력:
# continue
# B point

for v in generator_ex1():
    print(v)

#출력:
# start
# A point
# continue
# B point
# end

temp2 = [x*3 for x in generator_ex1()]
temp3 = (x*3 for x in generator_ex1())

print(temp2) #출력: ['A pointA pointA point', 'B pointB pointB point']
print(temp3) # 출력: ['A pointA pointA point', 'B pointB pointB point']

for i in temp3:
    print(i)
    # 출력:
    # start
    # A pointA pointA point
    # continue
    # B pointB pointB point
    # end

 

1) 제너레이터 함수

# filterfalse, takewhile, accumulate, chain, product, groupby..

import itertools

gen1 = itertools.count(1,2.5) # 무한대의 반복가능한 객체를 만드는 함수. 시작값은 1에서 2.5씩 더해서 무한대수를 만들어줌 -> while 사용하면 안됨

print(next(gen1)) #출력: 1
print(next(gen1)) #출력: 3.5
print(next(gen1)) #출력: 6.0
print(next(gen1)) #출력: 8.5
print(next(gen1)) #출력: 11.0

# 조건
gen2 = itertools.takewhile(lambda n : n < 50,itertools.count(1,5)) # takewhile은 제한을 거는 함수

for v in gen2:
    print(v,end=" ") #출력: 1 6 11 16 21 26 31 36 41 46
print()

gen3 = itertools.filterfalse(lambda n:n<3,[1,2,3,4,5]) #filterfalse는 필터의 반대역할을 하는 함수 -> false만 출력
for v in gen3:
    print(v)
#출력:
# 3
# 4
# 5

#누적 합계
gen4 = itertools.accumulate([x for x in range(1,11)])

for v in gen4:
    print(v,end=" ") #출력: 1 3 6 10 15 21 28 36 45 55
print()

# 연결
gen5 = itertools.chain('abcde',range(1,5)) # chain은 서로 다른 두개의 iterable 객체를 연결하는 함수
print(list(gen5)) # 출력: ['a', 'b', 'c', 'd', 'e', 1, 2, 3, 4]

gen6 = itertools.chain(enumerate('ABCDE')) # 인덱스와 값을 튜플형태로 연결함

print(list(gen6)) #출력: [(0, 'A'), (1, 'B'), (2, 'C'), (3, 'D'), (4, 'E')]

# 순서쌍(경우의 수)을 만들어주는 함수
gen7 = itertools.product("abcde")
print(list(gen7)) #출력: [('a',), ('b',), ('c',), ('d',), ('e',)]

gen8 = itertools.product("abc",repeat=3) # repeat은 순서쌍의 개수
print(list(gen8))
#출력:
# [('a', 'a', 'a'), ('a', 'a', 'b'), ('a', 'a', 'c'), ('a', 'b', 'a'), ('a', 'b', 'b'),
# ('a', 'b', 'c'), ('a', 'c', 'a'), ('a', 'c', 'b'), ('a', 'c', 'c'), ('b', 'a', 'a'),
# ('b', 'a', 'b'), ('b', 'a', 'c'), ('b', 'b', 'a'), ('b', 'b', 'b'), ('b', 'b', 'c'),
# ('b', 'c', 'a'), ('b', 'c', 'b'), ('b', 'c', 'c'), ('c', 'a', 'a'), ('c', 'a', 'b'),
# ('c', 'a', 'c'), ('c', 'b', 'a'), ('c', 'b', 'b'), ('c', 'b', 'c'), ('c', 'c', 'a'),
# ('c', 'c', 'b'), ('c', 'c', 'c')]

# 그룹화
gen9 = itertools.groupby("aaabbbcccddeee")

for chr, group in gen9:
    print(chr, ":", list(group))

#출력
# a : ['a', 'a', 'a']
# b : ['b', 'b', 'b']
# c : ['c', 'c', 'c']
# d : ['d', 'd']
# e : ['e', 'e', 'e']