[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개 일 때보다 다수의 프로세스를 사용 할 때 유용하다.