상세 컨텐츠

본문 제목

[인프런 리프 2기] 11. 파이썬 중급 과정 4주차(1) 병행성

경험/2021 인프런리프2기

by mizu-umi 2021. 3. 31. 11:36

본문

728x90

관련글 ▼

[인프런 리프 2기] 09. 파이썬 중급 과정 3주차(2) 시퀀스-3,4

[인프런 리프 2기] 10. 파이썬 중급 과정 3주차(3) 일급함수-1,2

[인프런 리프 2기] 10. 파이썬 중급 과정 3주차(4) 일급함수-3,4

 


 

 

파이썬 병행성 (1) 기본

  • 병행성 흐름제어 설명
  • 이터레이터
  • 제너레이터
  • __iter__, __next
  • 클래스 기반 제너레이터 구현

 


 

병행성 Concurrency

이터레이터 제너레이터

iterator, genertor 반복자 / 생성자

generator : 반복 가능한 객체를 생산하는 반복자(iterator)를 생성하는 함수

 

파이썬에서 반복 가능한 자료형 Iterable Data Type

collections , text? str?, list, dict, set, tuple, unpacking, *args 등등, 즉, dir() 함수로 속성(attribute)을 확인해보았을때 __iter__을 갖는 형들

 

t = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

print(dir(t))

for c in t:
    print(c)

dir()함수를 이용해 t라는 string이 directory를 열어 attribute를 확인해보면 __iter__이라는 속성을 발견할 수 있다. 따라서, t는 iterable이다.

 

w = iter(t)

print(next(w)) 
print(next(w)) 
print(next(w)) 
....

w라는 인스턴스에 iter() 함수로 t를 호출하면 w는 iterator object가 된다. 따라서 next()함수로 값을 하나하나 불러올 수 있다. 

 

iter()함수로 호출한 객체안의 길이(t로 예를 들면 알파벳 개수)만큼 next()함수로 불러올 수 있다. 정해진 index 이상을 next()로 호출하려 하면 StopIteration이라는 오류가 발생한다.

 

t.__iter__()

이건 t에 __iter__ 속성을 바로 호출한 경우.

 

while문을 사용해보자

while True:
    try:
        print(next(w))
    except StopIteration: # 예외처리
        break

앞서 for문으로 작성한 것을 while문으로 바꿔보았다. True일 동안 w의 값을 출력하도록 try하고 StopIteration이라는 오류가 발생하면 break하라는 코드다. for문 내부의 수행문이 작동하는 방식이 위의 while문과 같다고 보면 된다.

 


반복형을 확인하는 방법

1. hasattr (has attribute)

print(hasattr(t, '__iter__')) 

hasattr(object, 'attribute') 함수 : 해당 object가 해당 attribute를 갖는지 확인하는 함수이다.

 

2. abc (abstract class)

from collections import abc

print(isinstance(t, abc.Iterable))

collections 패키지에 있는 모듈들은 모두 iterable하다. print문에 작성된 내용을 문장으로 풀어보면

 

is instance t abtract class' iterable?

 

즉, t라는 인스턴스는 abtract class의 iterable이냐? 라는 의미로 실행해보면 맞으면 True 틀리면 False 가 출력된다.

 

※ instance vs object

같은 걸 나타내는 말인데 object는 하나의 객체로써 보는 거고 instance는 함수와의 상관성으로 보는 것.

 


클래스 기반 제너레이터

1. next()  패턴 : __next__를 호출하도록 만들었을 때

class WordSplitter:
    def __init__(self, text):
        self._idx = 0
        self._text = text.split(',')

    def __next__(self):
        print('Called __next__')
        try:
             word = self._text[self._idx]
        except IndexError:
            raise StopIteration('Stopped Iteration.')
        self._idx += 1
        return word

    def __repr__(self):
        return 'WordSplit(%s)' % (self._text)

위의 클래스를 객체에 할당해서 출력해보자.

 

wi = WordSplitter('x,y,z')

print(wi)

print(next(wi))
print(next(wi))
print(next(wi))
...

wi를 출력하면 generator라는 걸 알 수 있다. next()함수를 이용해 출력하면 순서대로 x,  y, z가 출력된다.

 

만약 여기서 print(next(wi))가 하나 더 작성될 경우, 앞서 WordSplitter의 __next__안에 작성한 예외처리로 인해 오류가 발생한다.

 

2. Generator 패턴 : yield를 사용해보자

class WordSplitGenerator:
    def __init__(self, text):
        self._text = text.split(' ')

    def __iter__(self):
        for word in self._text:
            yield word # 제너레이터(클래스기반)
        return # yield 사용했으므로 굳이 없어도됨

    def __repr__(self):
        return 'WordSplitGenerator(%s)' % (self._text)

yield 키워드를 사용해서 앞서 WordSplitter에서 복잡하게 작성한 코드를 단축시켰다.

 

wg = WordSplitGenerator('x y z')

wt = iter(wg)

print(wt)

print(wg)

print(next(wt))
print(next(wt))
print(next(wt))

인스턴스가 두개 필요하다는 단점은 있지만 class에서 수행해야하는 양은 줄어드므로 속도가 훨씬 빠를 것 같다.(내 뇌피셜)

 

다시 한번 정리해보는 제너레이터의 장점.

  1. 지능형리스트, 딕셔너리, 집합 -> 데이터 양이 증가할수록 메모리 사용 공간이 커지므로 제너레이터를 권장.
  2. 단위 실행이 가능한 코루틴 구현과 연동
  3. 작은 메모리 조각을 사용한다 (1번과 이어짐)

 

처음에는 generator의 장점들이 잘 와닿지 않았는데 이번에 코드를 작성해보면서 확실히 알 수 있었다. 지능형 리스트를 구현할 경우 한번에 모든 값을 출력하려고 하기 때문에 시간도 메모리도 많이 사용하게 된다. 반면 제너레이터는 값이 생성되기 직전의 대기 상태에 있기 때문에 원하는 값만큼 불러와서 사용할 수 있다.

 


 

 

 

파이썬 병행성 (2) 제너레이터

  • 제너레이터실습
  • yield실습
  • itertools 실습

 


병행성 vs 병렬성

  • 병행성(Concurrency) : 하나의 컴퓨터가 여러 작업을 동시에 수행. 파이썬의 큰 장점. 코루틴. CPU는 하나지만 여러 일을 동시에 하는 것처럼 할 수 있기 때문에 효율이 좋다. -> 단일 프로그램 안에서 여러가지 미션을 쉽게 해결해준다.
  • 병렬성(Parallelism) : 여러 컴퓨터가 여러 작업을 동시에 수행. 취합은 한 곳에서. -> 속도가 장점.

 


Generator 예제1

def generator_ex1():
    print('--Start')
    yield 'A Point'
    print('--Continue')
    yield 'B Point'
    print('--End')
    
temp = iter(generator_ex1())

print(temp) 

print(next(temp))
print(next(temp))
print(next(temp)) 

yield 키워드는 어떤 수행이 있을 때 해당 수행이 멈추는 구간을 지정해준다. 마지막 print(next(temp))는 그 아래로 수행할 코드가 더이상 존재하지 않으므로 StopIteration 이라는 error가 호출된다.

 

Generator 예제2

temp2 = [x * 3 for x in generator_ex1()] # 리스트
temp3 = (x * 3 for x in generator_ex1()) # generator

print(temp2) 
print(temp3) 

앞서 작성한 generator_ex1을 기반으로 temp2라는 리스트와 temp3이라는 generator를 작성했다. print문을 출력해보면 아래와 같다.

# temp2
A PointA PointA Point
B PointB PointB Point

# temp3
--Start
A PointA PointA Point
--Continue
B PointB PointB Point
--End

왜 temp2에서 generator_ex1의 print문은 출력되지 않았을까? print문은 iterable이 아니기때문이다.

 

Generator Ex3 : 중요함수 itertools

count, takewhile, filterfalse, accumulate, chain, product, groupby 등, itertools에 있는 함수들을 호출해서 제너레이터를 활용할 수 있다. 필요한 만큼의 값을 한꺼번에 가져오는 게 아니라 하나씩 생성할 수 있어서 제너레이터가 좋다

 

1. count 함수

gen1 = itertools.count(1, 2.5)

count(int, interval) : 사용하면 무한으로 값을 만들어낸다.

 

2. takewhile 함수

gen2 = itertools.takewhile(lambda n: n < 1000, itertools.count(1, 2.5))

takewhile (func object, range) : 함수에서 실행되는 동안의 값만 구해라.

 

3. filterfalse 함수

gen3 = itertools.filterfalse(lambda n : n < 3, [1,2,3,4,5])

filterfalse(func object, range) : filter 와는 반대되는 값을 구해라.

 

for v in gen3:

    print(v)

3보다 작은 1,2가 아니라 반대되는 3과 같거나 큰 3,4,5가 출력됨.

 

4. accumulate 함수

gen4 = itertools.accumulate([ x for x in range(1,101)])

accumulate(list) : 누적합계를 구해준다

 

5. chain 함수

: 객체와 객체를 연결해주는 함수

 

1. 예제1

gen5 = itertools.chain('ABCD', range(1,11,2))

chain(object1, object2, ...objectN) or chain(range)

 

2. 예제2

gen6 = itertools.chain(enumerate('ABCDE'))

enumerate(object) : object에 index번호를 붙여서 tuple로 만들어준다.

 

6. product 함수

product(object) : 개별로 분리해서 tuple 값으로 만들어줌

 

1. 예제1

gen7 = itertools.product('ABCDE')

위의 값을 출력해보면 ([('A',), ('B',), ('C',), ('D',), ('E',)] 이런 모양의 리스트가 출력된다.

 

2. 예제2

gen8 = itertools.product('ABCDE', repeat=1)

repeat 옵션 : = 을 통해 할당되는 숫자만큼의 모든 경우의 수를 반복해서 구해준다.

 

7. groupby 함수

gen9 = itertools.groupby('AAABBCCCCCDDEEEE')

호출되는 객체를 그루핑해주는 함수이다. print문을 이용해 출력할 수도 있지만 print문이 있을 경우에는 아래의 for문이 출력되지 않는다.

 

for chr, group in gen9:

    print(chr, ' : ', list(group))

 


 

 

 

파이썬 병행성 (3) 코루틴, yield

  • 흐름제어,병행성처리
  • 메인루틴 <-> 서브루틴
  • 쓰레드 차이 설명
  • 제너레이터 -> 코루틴 설명

 

회사에서도 그렇고 선생님도 비용이 많이 든다는 말을 자주 쓰시는데 비용 하면 돈부터 생각나서 요점을 잡아내지 못했는데, 코딩에서 비용이 많이 든다=속도가 느리다는 얘기였다.

 


스레드와 코루틴

스레드 thread

OS에서 직접 관리하며 병행성과 병렬성을 모두 가진다. CPU코어에서 실시간 또는 시분할로 비동기 작업(multi-thread). 싱글 스레드로도 멀티 스레드로도 사용할 수 있는 반면 코딩하기가 복잡하다.

 

왜? 자원을 공유하기 때문에 교착상태가 발생할 가능성이 높다. 컨텍스트 스위칭(Context switch) 비용이 크게 발생해서 자원을 소비할 가능성이 증가된다.

 

코루틴(Coroutine)

단일(single) 스레드. 스택을 기반으로 동작하는 비동기 작업. 단일 스레드에서도 여러가지 작업이 block되지 않도록 도와준다.

 

  • 서브루틴 : 메인루틴에서 호출(함수를 호출할때 호출되는 함수가 서브루틴이라고 보면 됨)해서 서브루틴에서 수행(흐름제어)
  • 코루틴 : 루틴을 실행하는 중에 중지했다가 재실행이 가능 -> 동시성 프로그래밍이라고도 함
  • 장점: 스레드에 비해 오버헤드가 감소한다.

yield : 메인 루틴과 서브루틴의 상태를 저장하고 양방향으로 데이터를 전송한다.

 

파이썬 3.5 이상에서 def 키워드를 async, yield라는 키워드를 await으로 사용가능. 완전히 비동기화 할때는 통일 시켜야하는 부분.

 

예제1

# 서브루틴
def coroutine1():
    print('>>> coroutine started')
    i = yield
    print('>>> coroutine recieved : {}'.format(i))
    
# 메인루틴
cr1 = coroutine1()
print(cr1, type(cr1))

코루틴은 제너레이터를 받기 때문에 어떤 책에서는 제너레이터 기반 코루틴이라고 말하기도 한다.

 

next(cr1)

cr1.send(100)

next()함수로 cr1의 첫번째 값을 출력한 다음, 그 다음 값도 next()함수로 출력하면 앞서 작성한 format에 None이라고 출력된다.

 

  • send() 함수 : 메인 루틴에서 작성한 데이터를 서브루틴에서 yield를 할당받은 i에게 보내서 저장시킨다. next()의 역할도 수행하므로 started에 이어 received도 출력된다. send()에서 ()가 비거나 next()로만 하면 값이 전송할 값도 출력할 값도 없으므로 None이 나온다.

 

잘못된 사용

cr2 = coroutine1()

cr2.send(100)

위의 값을 출력하면 무조건 오류가 발생한다. next()를 이용해서 coroutine의 yield까지는 먼저 출력 되어야 한다. yield가 있는 지점까지 수행이 되지 않은 상태에서 send를 입력하면 오류가 발생한다. 참고로 yield는 꼭 중간지점에 들어가야한다. yield를 수행문들보다 먼저 할당해버리면 TypeError: can't send non-None value to a just-started generator 라는 오류가 난다.

 

예제2 : 상태값 확인

def coroutine2(x):
    print('>>> coroutine started : {}'.format(x))
    y = yield x
    print('>>> coroutine received : {}'.format(y))
    z = yield x + y
    print('>>> coroutine received : {}'.format(z))

cr3 = coroutine2(10)

coroutine2라는 함수에는 yield가 두번 작성되어 있다. 해당 generator가 어떤 상태에 있는지를 확인하려면 모듈을 불러와야한다.

 

from inspect import getgeneratorstate

print(getgeneratorstate(cr3))

getgeneratorstate라는 모듈에서 상태 값의 종류는 아래와 같다.

 

  • GEN_CREATED : 처음 대기상태
  • GEN_RUNNING : 실행 상태
  • GEN_SUSPENDED : yield 대기 상태 (중요)
  • GEN_CLOSED : 실행완료

 

cr3.__next__()

print(getgeneratorstate(cr3))

cr3.send(100)

중간에 getgenratorstate를 출력해보면 send()가 호출되기 전까지는 GEN_SUSPENDED 상태에 머무른다.

 

print(cr3.send(100))

앞서 cr3.__next__()는 cr3의 next 값만이 출력된다면 위의 코드에서는 수행문에서 yield까지가 전부 출력된다.

 

예제3 : 중첩 코루틴 nested coroutine

def generator1():
    for x in 'AB':
        yield x
    for y in range(1,4):
        yield y

generator1이라는 함수에 for문이 두번 들어갔다. 이걸 좀 더 간략하게 만들어보면

 

def generator2():
    yield from 'AB'
    yield from range(1,4)

yield from을 사용하면 된다.

 

강의 중에는 설명 안해주니까 덧붙이는 말. yield는 영어로 수확한다는 의미를 갖는다. yield from 'AB'는 [AB에서 수확한다]라는 의미로 볼 수 있다. 어떠한 자료든 iterable이라면 yield할 수 있다.

 


 

이번 병행성 강의를 전체적으로 들으면서 내가 전보다는 파이썬을 더 잘 이해하게 됐다는 걸 알았다! 무엇보다 프로그래밍 언어가 영어로 되어있다보니 이따금씩 원래 의미와는 조금 다르게 사용되는 친구들 제외하고는 직관적인 사용법을 가지고 있어서, 어떤 객체가 어떤 attribute를 갖고 또 어떤 구조가 iterator인지 혹은 generator인지 구분할 수 있게 되었다. 앞으로 남은 건 연습과 실습 뿐이다...!

 


< 강의 출처 - 인프런 우리를 위한 프로그래밍 파이썬 중급 (Inflearn Original) >

 

728x90
반응형

관련글 더보기

댓글 영역