Programming,  Python

[Python] Coroutine

기존에 generator에서 yield 키워드를 lvalue로 사용해 제어권을 넘겨줬지만, coroutine은 rvalue로 사용하여 특정 변수에 값을 전달 할 수 있다. 참고로 Coroutine은 generator의 일종이다.

def receiver():
    print("Ready to receive")
    while True:
        n = yield
        print("Got %s" % n)
r = receiver()
next(r)  # Ready to receive
r.send(1) # Got 1
r.send(2) # Got 2
r.send("Hello") # Got Hello
Ready to receive
Got 1
Got 2
Got Hello

Coroutine은 decorator를 통해 사용하기 유용하다. 아래 예시 코드를 활용 가능하다.

def coroutine(func):
    def start(*args, **kwargs):
        g = func(*args, **kwargs)
        next(g)
        return g
    return start

# receiver = coroutine(receiver)
@coroutine
def receiver():
    print("Ready to receive")
    while True:
        n = (yield)
        print("Got %s" % n)

r = receiver() # Ready to receive
r.send("Hello World") # Got Hello World

r.close()
r.send(4)
Ready to receive
Got Hello World

StopIteration Traceback (most recent call last)
in
1 r.close()
—-> 2 r.send(4)
StopIteration:

위 코드를 설명해보자면, @coroutine은 decorator로 사용되는데 이를 좀 더 풀어서 쓰자면 receiver = coroutine(receiver) 가 될 수 있다. coroutine 함수는 return이 closure로 되어있다.

처음 r = receiver() 가 수행되면 receiver() 함수의 yield 키워드 전까지 수행된다. Decorator의 동작에는 해당 함수의 next()를 수행하도록 기술되어있기 때문에, 선언을 하자마자 바로 yield 전까지 간다.

r.close()를 하게되면 coroutine 수행이 종료되기 때문에 이후에 다시 send()를 하면 error가 발생한다.

생성기와 Coroutine 활용

import os
import fnmatch
def find_files(topdir, pattern):
    # os.walk: file open in current directory
    for path, dirname, filelist in os.walk(topdir):
        for name in filelist:
            # fnmatch: file name match function
            if fnmatch.fnmatch(name, pattern):
                yield os.path.join(path,name)

def opener(filenames):
    for name in filenames:
        f = open(name)
        yield f
        
def cat(filelist):
    for f in filelist:
        for line in f:
            yield line
            
def grep(pattern, filelist):
    for line in lines:
        if pattern in line:
            yield line
passwd = find_files(".", "passwd")
files = opener(passwd)
lines = cat(files)
pylines = grep("linux", lines)
for line in pylines:
    print(line)

위 코드는 특정 디렉토리에 있는 특정 파일 중에 찾고 싶은 단어를 찾아서 해당 단어를 포함하는 문장을 출력시키는 프로그램이다. (coroutine 없이 generator 사용)

find_files 라는 generator를 통해서 찾고자 하는 파일 정보까지 획득한 후에 yield를 하여 다시 main function으로 넘어온다. opener, cat, grep generator에서 file descriptor, file line, pattern을 찾아서 각 조건에 부합 할때마다 yield가 발생되는 프로그램이다.
Coroutine을 통해 해당 프로그램을 작성해보면 다음과 같다.

import os
import fnmatch

@coroutine
def find_files(target):
    while True:
        topdir, pattern = (yield)
        for path, dirname, filelist in os.walk(topdir):
            for name in filelist:
                if fnmatch.fnmatch(name, pattern):
                    target.send(os.path.join(path,name))

@coroutine                
def opener(target):
    while True:
        name = (yield)
        f = open(name)
        target.send(f)
        
@coroutine                
def cat(target):
    while True:
        f = (yield)
        for line in f:
            target.send(line)

@coroutine                
def grep(pattern, target):
    while True:
        line = (yield)
        if pattern in line:
            target.send(line)
            
@coroutine                
def printer():
    while True:
        line = (yield)
        print(line)
finder = find_files(opener(cat(grep("linux", printer()))))
finder.send((".", "passwd"))

처음 시작은 모든 함수는 yield에서 멈춰있는 상태다. 이때 finder.send((".", "passwd")) 를 통해 값이 입력되면 차례차례 전달된다.

프로세스가 1개 일 때보다 다수의 프로세스를 사용 할 때 유용하다.

Leave a Reply

Your email address will not be published. Required fields are marked *