티스토리 뷰
Reference
- Fluent Python
- Python CookBook
yield from을 통해 서브루틴 만들기
파이썬 3.4부터 yield from이라는 새로운 키워드가 추가되었다.
def gen():
for c in 'AB':
yield c
for i in range(1, 3):
yield i
print(list(gen()))
>>> ['A', 'B', 1, 2]
보통 제너레이터 함수 내에서 또 다른 루틴이 있을 때
for문을 사용함으로써 해당 루틴을 표현하면 되지만
def gen():
yield from 'AB'
yield from range(1, 3)
print(list(gen()))
>>> ['A', 'B', 1, 2]
yield from구문을 사용하면 더 깔끔하게 해결할 수 있다.
yield from 'x' 표현식이 'x'객체에 대해 첫 번째로 하는 일은
iter(x)를 호출해서 x의 반복자를 가져오는 것이다.
for문에서도 동일하게 __iter__()를 통해 반복자를 가져온 다음
next() 함수와 StopIteration예외처리를 통해
해당 반복자의 값들을 전부 다 생성한 뒤
예외를 내부적으로 잡아서 자동으로 처리한다.
(★ 서브루틴으로써의 yield from은 사실상 for 루프와 동작이 같다. ★)
똑같이 반복자를 생성하고 next()를 통해 값을 생성하며
StopIteration예외처리를 통해 예외를 내부적으로 처리한다.
--> 이러한 이유 때문에 yield from을 for 루프 안의 yield에 대한 단축 문으로 사용할 수 있다.
# yield from 사용 (o)
from collections import abc
def flatten(items, ignore_types=(str, bytes)):
for x in items:
if isinstance(x, abc.Iterable) and not isinstance(x, ignore_types):
yield from flatten(x)
else:
yield x
# yield from 사용 (x)
def flatten(items, ignore_types=(str, bytes)):
for x in items:
if isinstance(x, abc.Iterable) and not isinstance(x, ignore_types):
for i in flatten(x):
yield i
else:
yield x
items = [1, 2, [3, 4, [5, 6], 7], 8]
print(list(flatten(items)))
>>> [1, 2, 3, 4, 5, 6, 7, 8]
items = ['Dave', 'Paula', ['Thomas', 'Lewis']]
print(list(flatten(items)))
>>> ['Dave', 'Paula', 'Thomas', 'Lewis']
위 코드는 Python CookBook에 있는 코드인데
재귀적인 처리를 보여주는 면에서
조금 더 복잡하지만 유용하다.
※ 참고로 str과 bytes 같은 경우는 자체가 이터러블한 객체이기 대문에
'Antoliny'같은 문자열 값을 보내면 값을 하나하나 순환하게 돼버린다.
(isinstance x='Antoliny', abc.Iterable) --> True
'A'
'n'
't'
...
이런 부분을 방지하기 위함이다 --> ignore_types=(str, bytes)
큰 차이는 없는 것 같지만
이렇게 제너레이터 함수 내에서 서브루틴으로써 다른 제너레이터를 호출할 때는
yield from을 사용하면 더 편리하고
코드도 더 보기 좋다.
코루틴에서의 yield from
뭔가 아쉽다.
지금까지 봐왔던 내용으로 보면
사실상 yield from이 for문을 대체하는 기능 말고는
딱히 유용하지 않은 것 같다는 느낌이 들 수 있지만
yield from은
코루틴으로 사용함으로써 메인 루틴과 서브 루틴이 값을 주고받게 할 때
진정한 가치를 보인다.
조금 긴 코드를 보기 전에 코루틴에서의 yield from을 간단하게 설명하자면
대표 제너레이터(메인 루틴)가 yield from을 통해 서브루틴을 생성하고
호출자를 통해 서브루틴에 값을 보낸 뒤 서브루틴이 종료되면
서브루틴의 리턴 값을 대표 제너레이터가 사용할 수 있게 되는 형식이다.
글보다는 코드로 보는 게 더 이해하기 편할 거라고 생각한다.
from collections import namedtuple
Result = namedtuple('Result', 'count average')
# 하위 제너레이터
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield # (6)
if term is None: # (8)
break
total += term
count += 1
average = total/count
return Result(count, average) # (9)
# 대표 제너레이터
def grouper(results, key):
while True:
results[key] = yield from averager() # (5), (10)
def main(data):
results = {}
for key, values in data.items(): # (1)
group = grouper(results, key) # (2)
next(group) # (3)
for value in values: # (4)
group.send(value)
group.send(None) # (7)
report(results)
def report(results):
for key, result in sorted(results.items()):
group, unit = key.split(';')
print('{:2} {:5} averaging {:.2f}{}'.format(
result.count, group, result.average, unit))
data = {
'girls;kg':
[40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
'girls;m':
[1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
'boys;kg':
[39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
'boys;m':
[1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}
if __name__ == '__main__':
main(data)
-코드에 대한 설명- (남/녀 학생 평균 키, 몸무게를 구하는 코드이다)
1. data변수의(딕셔너리형) key와 value값을 가져온다. (for문이라는 걸 기억하자 1 ~ 10번이 반복된다)
2. grouper(results=딕셔너리, key) 객체를 생성한다.
3. next(group)을 통해 코루틴(grouper)을 가동해서 yield from을 통해 averager() 객체를 생성하고 대기한다.(averager() 객체가 종료될 때까지)
4.★ for반복문을 통해 value값(리스트)들을 grouper객체에 보내주는 거 같지만 ★
5.★ yield from에 의해 grouper객체가 아닌 averager()코루틴 객체로 전달된다.(grouper는 averager가 종료되기 전까지 대기상태) ★
6. averager() 객체로 보내진 값은 term변수에 할당된다.(그리고 아래에서 값이 보내질 때마다 평균을 계속 구한다 [무한반복])
7. main함수에서 value값을 전부 다 순회하고 종료되면 averager객체에 None을 보낸다.
8. term에 None이 할당되고 제어문을 통해 코루틴을 종료하고 return문으로 가게 된다(while문을 탈출했기 때문에).
9. 네임드튜플 객체를 반환한다.(보통은 StopIteration예외가 발생하지만
yield from은 StopIteration예외 처리가 되어있고 value속성이 yieldfrom 표현식의 값이 됨
10.★ results[key]에 averager의 리턴 값인 StopIteration.value값 --> 네임드튜플 객체가 할당된다. ★
위 예제 사례를 보면 알 수 있듯이
yield from을 통해 서브루틴을 생성하고
메인루틴에 연결된 파이프를 타고 send() 값이 서브루틴에게 향하는 구조이다.
그리고 서브루틴이 종료되면(만약 서브루틴을 종료하지 않는다면 계속 서브루틴에만 머물게 된다.)
서브루틴의 리턴값이 메인루틴의 값이 되는 구조이다.
grouper는 서브루틴(averager)이 하나였지만
서브루틴에 yield from을 사용하면(본인도 누군가에겐 메인루틴이된다.) 결국 여러 개의 서브루틴을 만들 수 있다.
(마치 파이프처럼 서로가 연결되어 있다고 생각하면 된다.)
그리고 꼭 마지막 서브루틴은 yield를 사용해서 끝이 있어야 하고
이 값들이 파이프를 타고 최종 메인루틴으로 올라가는 식으로 진행되는 구조이다.
asyncio모듈을 이용한 비동기 프로그래밍과
yield from이 관련되어 있어서(이벤트 루프에 의존해서 작동)
yield from에 대한 동작 과정에 대해 이해가 필요하다고 생각한다.
사실 파이썬 3.5에서부터 async(네이티브 코루틴), await이라는 키워드를 제공하기 때문에
코루틴안에서 다른 서브루틴을 실행할 때는
yield from이 await으로 대체되었다.
(※ 위에서 본 예제 코드를 이해했다면 await(기다리다)라는 말이 더 의미가 명확하게 느껴질 것이다.)
오늘 알게 된 yield from(await)의 동작원리는
사실상 비동기 프로그래밍(Asynchronous)에 들어가기 전 준비작업이라고 할 수 있다.
마지막으로 asyncio를 아직 안 배웠거나 배우고 있더라도
stackoverflow에서 괜찮은 글을 하나 찾은 거 같아서
남기고 간다.
https://stackoverflow.com/questions/49005651/how-does-asyncio-actually-work
How does asyncio actually work?
This question is motivated by my another question: How to await in cdef? There are tons of articles and blog posts on the web about asyncio, but they are all very superficial. I couldn't find any
stackoverflow.com
후기
asyncio를 이용한 동시성 프로그래밍은
책 한 권으로 나와도 부족할 정도로
큰 파트라고 생각한다.
사실 필자도 아직 동시성, 병렬 프로그래밍 관련된 내용에는
굉장히 무지하기 때문에
열심히 노력해서 해당 글들을 포스팅하겠다.

'Python' 카테고리의 다른 글
[파이썬] 디스크립터(descriptor) - [2] (0) | 2022.07.25 |
---|---|
[파이썬]디스크립터(descriptor) - [1] (0) | 2022.07.21 |
[파이썬] 코루틴 (0) | 2022.07.11 |
[파이썬] 데코레이터 (0) | 2022.07.04 |
[파이썬]인터페이스, 덕타이핑, 프로토콜 (1) | 2022.06.25 |