Scribbling

Python: Coroutines 본문

Computer Science/Python

Python: Coroutines

focalpoint 2022. 5. 3. 11:57

 

Basic behavior of Coroutines

def coroutine():
    print('started')
    x = yield
    print('received: ', x)

c = coroutine()
next(c)
c.send(3)

1) "next(c)" or "c.send(None)" primes the coroutine -> coroutine now waits at 'yield' expression

2) c.send(3) sets x to 3, re-executing the coroutine

3) At the end, coroutine raises StopIteration

 

Example: coroutine to compute a running average

def averager():
    total, count = .0, 0
    average = None
    while True:
        num = yield average
        total += num
        count += 1
        average = total / count

a = averager()
next(a)
print(a.send(3))
print(a.send(5))
print(a.send(11))

 

A decorator can be used to prime a coroutine conveniently.

from functools import wraps

def coroutine(func):
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer

@coroutine
def averager():
    total, count = .0, 0
    average = None
    while True:
        num = yield average
        total += num
        count += 1
        average = total / count

 

Generator.throw(): delivers an error to the generator. If the generator handles the exception, control flows until the next yield and generator.throw() receives the yielded value. If not handled, the exception is again delivered to the caller.

Generator.close(): raises GeneratorExit in the generator. If the generator does not handle the exception or raises StopIteration, none of the exceptions are delivered to the caller.

class SimpleException(Exception):
    ''' Test Exception '''

def coroutine():
    print('started')
    while True:
        try:
            x = yield
        except SimpleException:
            print('SimpleException handled. Continuing... ')
        else:
            print('received: ', x)

c = coroutine()
next(c)
c.send(11)
c.throw(SimpleException)
c.send(12)
c.throw(ZeroDivisionError)
c.send(13)
c.close()

If something should be executed with an unexpected exception, try/finally block can handle the case.

def coroutine():
    print('started')
    try:
        while True:
            try:
                x = yield
            except SimpleException:
                print('SimpleException handled. Continuing... ')
            else:
                print('received: ', x)
    finally:
        print('Unexpected Exception; Ending Coroutine... ')

 

Couroutines can return values. 

from collections import namedtuple

Result = namedtuple('result', 'count average')

def averager():
    total, count = .0, 0
    average = None
    while True:
        num = yield average
        if num is None:
            break
        total += num
        count += 1
        average = total / count
    return Result(count, average)


a = averager()
next(a)
print(a.send(3))
print(a.send(5))
print(a.send(11))
print(a.send(None))

a = averager()
next(a)
print(a.send(3))
print(a.send(5))
print(a.send(11))
try:
    a.send(None)
except StopIteration as exc:
    result = exc.value

print(result)

Returned value can be captured like above.

"yield from" is very useful in this case as it receives the returned value.

 

Using yield from

When generator is stopped at 'yield from', sub generator directly communicates with caller through send() and yield() methods. At the time when sub generator is completed, it raises StopIteration with returned value and generator comes to play again. 

from collections import namedtuple

Result = namedtuple('result', 'count average')

def averager():
    total, count = .0, 0
    average = None
    while True:
        num = yield average
        if num is None:
            break
        total += num
        count += 1
        average = total / count
    return Result(count, average)


def grouper(results, key):
    while True:
        results[key] = yield from averager()


def caller(data):
    results = {}
    for key, values in data.items():
        group = grouper(results, key)
        next(group)
        for value in values:
            group.send(value)
        group.send(None)
    print(results)

data = {
    'girls;kg': [41, 45, 47, 49, 51, 50.5, 49.5],
    'girls;m': [157, 159, 161, 167, 167.5, 163.3]
}

caller(data)

* keep in mind that averager() is called for four times in the above code: When StopIteration is raised from sub-generator, generator code runs again. Within while loop, grouper again stops at yield from calling averager at the same time. 

'Computer Science > Python' 카테고리의 다른 글

Zip & Unpacking  (0) 2022.05.10
Python: Concurrency with Futures  (0) 2022.05.09
Python: Context Manager  (0) 2022.04.29
Python: Iterator, Generator  (0) 2022.04.22
Python: Operator Overloading  (0) 2022.04.20