소개
이 랩에서는 Python 의 yield 문에서 발생하는 사항을 관리하는 방법을 배우게 됩니다. 이러한 문과 관련된 작업 및 동작을 효과적으로 처리하는 방법을 이해하게 될 것입니다.
또한, 제너레이터 수명 주기와 제너레이터의 예외 처리에 대해서도 배우게 됩니다. 이 학습 과정에서 follow.py 및 cofollow.py 파일이 수정될 것입니다.
이 랩에서는 Python 의 yield 문에서 발생하는 사항을 관리하는 방법을 배우게 됩니다. 이러한 문과 관련된 작업 및 동작을 효과적으로 처리하는 방법을 이해하게 될 것입니다.
또한, 제너레이터 수명 주기와 제너레이터의 예외 처리에 대해서도 배우게 됩니다. 이 학습 과정에서 follow.py 및 cofollow.py 파일이 수정될 것입니다.
이 단계에서는 Python 제너레이터의 수명 주기를 살펴보고 이를 적절하게 닫는 방법을 배우겠습니다. Python 의 제너레이터는 모든 값을 한 번에 계산하여 메모리에 저장하는 대신, 즉석에서 일련의 값을 생성할 수 있는 특수한 유형의 이터레이터입니다. 이는 대규모 데이터 세트 또는 무한 시퀀스를 처리할 때 매우 유용할 수 있습니다.
follow() 제너레이터란 무엇인가요?프로젝트 디렉토리의 follow.py 파일을 살펴보겠습니다. 이 파일에는 follow()라는 제너레이터 함수가 포함되어 있습니다. 제너레이터 함수는 일반 함수와 유사하게 정의되지만, return 키워드 대신 yield를 사용합니다. 제너레이터 함수가 호출되면 제너레이터 객체를 반환하며, 이 객체를 반복하여 yield 하는 값을 얻을 수 있습니다.
follow() 제너레이터 함수는 파일에서 지속적으로 줄을 읽고 읽는 대로 각 줄을 yield 합니다. 이는 새 줄에 대해 파일을 지속적으로 모니터링하는 Unix tail -f 명령과 유사합니다.
WebIDE 편집기에서 follow.py 파일을 엽니다.
import os
import time
def follow(filename):
with open(filename,'r') as f:
f.seek(0,os.SEEK_END)
while True:
line = f.readline()
if line == '':
time.sleep(0.1) ## Sleep briefly to avoid busy wait
continue
yield line
이 코드에서 with open(filename, 'r') as f 문은 파일을 읽기 모드로 열고 블록이 종료될 때 제대로 닫히도록 합니다. f.seek(0, os.SEEK_END) 줄은 파일 포인터를 파일의 끝으로 이동시켜 제너레이터가 끝에서부터 읽기를 시작하도록 합니다. while True 루프는 파일에서 지속적으로 줄을 읽습니다. 줄이 비어 있으면 아직 새 줄이 없다는 의미이므로 프로그램은 바쁜 대기를 피하기 위해 0.1 초 동안 대기한 다음 다음 반복으로 계속 진행합니다. 줄이 비어 있지 않으면 yield 됩니다.
이 제너레이터는 무한 루프에서 실행되므로 중요한 질문이 제기됩니다. 제너레이터 사용을 중단하거나 조기에 종료하려는 경우 어떻게 될까요?
제너레이터가 제대로 닫히는 경우를 처리하도록 follow.py의 follow() 함수를 수정해야 합니다. 이를 위해 GeneratorExit 예외를 catch 하는 try-except 블록을 추가합니다. GeneratorExit 예외는 가비지 수집 또는 close() 메서드를 호출하여 제너레이터가 닫힐 때 발생합니다.
import os
import time
def follow(filename):
try:
with open(filename,'r') as f:
f.seek(0,os.SEEK_END)
while True:
line = f.readline()
if line == '':
time.sleep(0.1) ## Sleep briefly to avoid busy wait
continue
yield line
except GeneratorExit:
print('Following Done')
이 수정된 코드에서 try 블록에는 제너레이터의 주요 로직이 포함되어 있습니다. GeneratorExit 예외가 발생하면 except 블록이 이를 catch 하고 'Following Done' 메시지를 출력합니다. 이는 제너레이터가 닫힐 때 정리 작업을 수행하는 간단한 방법입니다.
이러한 변경을 수행한 후 파일을 저장합니다.
이제 제너레이터가 가비지 수집되거나 명시적으로 닫힐 때 어떻게 동작하는지 확인하기 위해 몇 가지 실험을 수행해 보겠습니다.
터미널을 열고 Python 인터프리터를 실행합니다.
cd ~/project
python3
>>> from follow import follow
>>> ## Experiment: Garbage collection of a running generator
>>> f = follow('stocklog.csv')
>>> next(f)
'"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314\n'
>>> del f ## Delete the generator object
Following Done ## This message appears because of our GeneratorExit handler
이 실험에서는 먼저 follow.py 파일에서 follow 함수를 가져옵니다. 그런 다음 follow('stocklog.csv')를 호출하여 제너레이터 객체 f를 생성합니다. next() 함수를 사용하여 제너레이터에서 다음 줄을 가져옵니다. 마지막으로 del 문을 사용하여 제너레이터 객체를 삭제합니다. 제너레이터 객체가 삭제되면 자동으로 닫히고, 이는 GeneratorExit 예외 처리기를 트리거하여 'Following Done' 메시지가 출력됩니다.
>>> f = follow('stocklog.csv')
>>> for line in f:
... print(line, end='')
... if 'IBM' in line:
... f.close() ## Explicitly close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
Following Done
>>> for line in f:
... print(line, end='') ## No output: generator is closed
...
이 실험에서는 새 제너레이터 객체 f를 생성하고 for 루프를 사용하여 반복합니다. 루프 내에서 각 줄을 출력하고 해당 줄에 문자열 'IBM'이 포함되어 있는지 확인합니다. 포함되어 있으면 제너레이터에서 close() 메서드를 호출하여 명시적으로 닫습니다. 제너레이터가 닫히면 GeneratorExit 예외가 발생하고 예외 처리기가 'Following Done' 메시지를 출력합니다. 제너레이터가 닫힌 후 다시 반복하려고 하면 제너레이터가 더 이상 활성 상태가 아니므로 출력이 없습니다.
>>> f = follow('stocklog.csv')
>>> for line in f:
... print(line, end='')
... if 'IBM' in line:
... break ## Break out of the loop, but don't close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
>>> ## Resume iteration - the generator is still active
>>> for line in f:
... print(line, end='')
... if 'IBM' in line:
... break
...
"CAT",78.36,"6/11/2007","09:37.19",-0.16,78.32,78.36,77.99,237714
"VZ",42.99,"6/11/2007","09:37.20",-0.08,42.95,42.99,42.78,268459
"IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859
>>> del f ## Clean up
Following Done
이 실험에서는 제너레이터 객체 f를 생성하고 for 루프를 사용하여 반복합니다. 루프 내에서 각 줄을 출력하고 해당 줄에 문자열 'IBM'이 포함되어 있는지 확인합니다. 포함되어 있으면 break 문을 사용하여 루프에서 벗어납니다. 루프에서 벗어나는 것은 제너레이터를 닫지 않으므로 제너레이터는 여전히 활성 상태입니다. 그런 다음 동일한 제너레이터 객체에 대해 새 for 루프를 시작하여 반복을 다시 시작할 수 있습니다. 마지막으로 제너레이터 객체를 삭제하여 정리하면 GeneratorExit 예외 처리기가 트리거됩니다.
close() 호출을 통해) 제너레이터 내에서 GeneratorExit 예외가 발생합니다.break 사용) 은 제너레이터를 닫지 않으므로 나중에 다시 시작할 수 있습니다.exit()를 입력하거나 Ctrl+D를 눌러 Python 인터프리터를 종료합니다.
이 단계에서는 제너레이터와 코루틴에서 예외를 처리하는 방법을 배우겠습니다. 하지만 먼저 예외가 무엇인지 이해해 보겠습니다. 예외는 프로그램 실행 중에 발생하여 프로그램의 정상적인 흐름을 방해하는 이벤트입니다. Python 에서는 throw() 메서드를 사용하여 제너레이터와 코루틴에서 예외를 처리할 수 있습니다.
코루틴은 특수한 유형의 제너레이터입니다. 주로 값을 yield 하는 일반 제너레이터와 달리, 코루틴은 값을 소비 ( send() 메서드 사용) 하고 값을 yield 할 수 있습니다. cofollow.py 파일에는 코루틴의 간단한 구현이 있습니다.
WebIDE 편집기에서 cofollow.py 파일을 열어보겠습니다. 다음은 내부 코드입니다.
def consumer(func):
def start(*args,**kwargs):
c = func(*args,**kwargs)
next(c)
return c
return start
@consumer
def printer():
while True:
item = yield
print(item)
이제 이 코드를 분석해 보겠습니다. consumer는 데코레이터입니다. 데코레이터는 다른 함수를 인수로 받아 해당 함수에 일부 기능을 추가한 다음 수정된 함수를 반환하는 함수입니다. 이 경우 consumer 데코레이터는 자동으로 제너레이터를 첫 번째 yield 문으로 이동시킵니다. 이는 제너레이터가 값을 받을 준비가 되도록 하므로 중요합니다.
printer() 코루틴은 @consumer 데코레이터로 정의됩니다. printer() 함수 내부에는 무한 while 루프가 있습니다. item = yield 문은 마법이 일어나는 곳입니다. 코루틴의 실행을 일시 중지하고 값을 받을 때까지 기다립니다. 코루틴으로 값이 전송되면 실행을 재개하고 수신된 값을 출력합니다.
이제 printer() 코루틴을 수정하여 예외를 처리하겠습니다. cofollow.py의 printer() 함수를 다음과 같이 업데이트합니다.
@consumer
def printer():
while True:
try:
item = yield
print(item)
except Exception as e:
print('ERROR: %r' % e)
try 블록에는 예외를 발생시킬 수 있는 코드가 포함되어 있습니다. 이 경우 값을 받고 출력하는 코드입니다. try 블록에서 예외가 발생하면 실행이 except 블록으로 이동합니다. except 블록은 예외를 catch 하고 오류 메시지를 출력합니다. 이러한 변경을 수행한 후 파일을 저장합니다.
코루틴에 예외를 throw 하는 실험을 시작해 보겠습니다. 터미널을 열고 다음 명령을 사용하여 Python 인터프리터를 실행합니다.
cd ~/project
python3
>>> from cofollow import printer
>>> p = printer()
>>> p.send('hello') ## Send a value to the coroutine
hello
>>> p.send(42) ## Send another value
42
여기서는 먼저 cofollow 모듈에서 printer 코루틴을 가져옵니다. 그런 다음 p라는 printer 코루틴의 인스턴스를 생성합니다. send() 메서드를 사용하여 코루틴에 값을 보냅니다. 보시다시피 코루틴은 문제 없이 전송한 값을 처리합니다.
>>> p.throw(ValueError('It failed')) ## Throw an exception into the coroutine
ERROR: ValueError('It failed')
이 실험에서는 throw() 메서드를 사용하여 ValueError 예외를 코루틴에 주입합니다. printer() 코루틴의 try-except 블록은 예외를 catch 하고 오류 메시지를 출력합니다. 이는 예외 처리가 예상대로 작동하고 있음을 보여줍니다.
>>> try:
... int('n/a') ## This will raise a ValueError
... except ValueError as e:
... p.throw(e) ## Throw the caught exception into the coroutine
...
ERROR: ValueError("invalid literal for int() with base 10: 'n/a'")
여기서는 먼저 문자열 'n/a'를 정수로 변환하려고 시도하는데, 이로 인해 ValueError가 발생합니다. 이 예외를 catch 한 다음 throw() 메서드를 사용하여 코루틴에 전달합니다. 코루틴은 예외를 catch 하고 오류 메시지를 출력합니다.
>>> p.send('still working') ## The coroutine continues to run after handling exceptions
still working
예외를 처리한 후 send() 메서드를 사용하여 코루틴에 다른 값을 보냅니다. 코루틴은 여전히 활성 상태이며 새 값을 처리할 수 있습니다. 이는 오류가 발생한 후에도 코루틴이 계속 실행될 수 있음을 보여줍니다.
yield 문 지점에서 예외를 처리할 수 있습니다. 즉, 코루틴이 값을 기다리거나 처리할 때 발생하는 오류를 catch 하고 처리할 수 있습니다.throw() 메서드를 사용하면 예외를 제너레이터 또는 코루틴에 주입할 수 있습니다. 이는 테스트 및 코루틴 외부에서 발생하는 오류를 처리하는 데 유용합니다.Python 인터프리터를 종료하려면 exit()를 입력하거나 Ctrl+D를 누르면 됩니다.
이 단계에서는 제너레이터 관리 및 제너레이터에서 예외를 처리하는 방법에 대해 배운 개념을 실제 시나리오에 적용하는 방법을 살펴보겠습니다. 이러한 실용적인 응용을 이해하면 더 강력하고 효율적인 Python 코드를 작성하는 데 도움이 됩니다.
파일 모니터링 시스템의 더 안정적인 버전을 구축해 보겠습니다. 이 시스템은 시간 초과 및 중지 요청과 같은 다양한 상황을 처리할 수 있습니다.
먼저 WebIDE 편집기를 열고 robust_follow.py라는 새 파일을 만듭니다. 이 파일에 작성해야 하는 코드는 다음과 같습니다.
import os
import time
import signal
class TimeoutError(Exception):
pass
def timeout_handler(signum, frame):
raise TimeoutError("Operation timed out")
def follow(filename, timeout=None):
"""
A generator that yields new lines in a file.
With timeout handling and proper cleanup.
"""
try:
## Set up timeout if specified
if timeout:
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout)
with open(filename, 'r') as f:
f.seek(0, os.SEEK_END)
while True:
line = f.readline()
if line == '':
## No new data, wait briefly
time.sleep(0.1)
continue
yield line
except TimeoutError:
print(f"Following timed out after {timeout} seconds")
except GeneratorExit:
print("Following stopped by request")
finally:
## Clean up timeout alarm if it was set
if timeout:
signal.alarm(0)
print("Follow generator cleanup complete")
이 코드에서는 먼저 사용자 지정 TimeoutError 클래스를 정의합니다. timeout_handler 함수는 시간 초과가 발생할 때 이 오류를 발생시키는 데 사용됩니다. follow 함수는 파일을 읽고 새 줄을 yield 하는 제너레이터입니다. 시간 초과가 지정되면 signal 모듈을 사용하여 알람을 설정합니다. 파일에 새 데이터가 없으면 잠시 기다린 후 다시 시도합니다. try - except - finally 블록은 다양한 예외를 처리하고 적절한 정리를 보장하는 데 사용됩니다.
코드를 작성한 후 파일을 저장합니다.
이제 개선된 파일 모니터링 시스템을 테스트해 보겠습니다. 터미널을 열고 다음 명령으로 Python 인터프리터를 실행합니다.
cd ~/project
python3
Python 인터프리터에서 follow 제너레이터의 기본 기능을 테스트합니다. 실행할 코드는 다음과 같습니다.
>>> from robust_follow import follow
>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
... print(f"Line {i+1}: {line.strip()}")
... if i >= 2: ## Just read a few lines for the example
... break
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Line 3: "HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
여기서는 robust_follow.py 파일에서 follow 함수를 가져옵니다. 그런 다음 stocklog.csv 파일을 따르는 제너레이터 객체 f를 생성합니다. for 루프를 사용하여 제너레이터가 yield 하는 줄을 반복하고 처음 세 줄을 출력합니다.
시간 초과 기능이 어떻게 작동하는지 살펴보겠습니다. Python 인터프리터에서 다음 코드를 실행합니다.
>>> ## Create a generator that will time out after 3 seconds
>>> f = follow('stocklog.csv', timeout=3)
>>> for line in f:
... print(line.strip())
... time.sleep(1) ## Process each line slowly
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
Following timed out after 3 seconds
Follow generator cleanup complete
이 실험에서는 3 초 시간 초과가 있는 제너레이터를 생성합니다. 각 줄 사이에 1 초 동안 대기하여 각 줄을 천천히 처리합니다. 약 3 초 후에 제너레이터는 시간 초과 예외를 발생시키고 finally 블록의 정리 코드가 실행됩니다.
제너레이터가 명시적 클로저를 어떻게 처리하는지 테스트해 보겠습니다. 다음 코드를 실행합니다.
>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
... print(f"Line {i+1}: {line.strip()}")
... if i >= 1:
... print("Explicitly closing the generator...")
... f.close()
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Explicitly closing the generator...
Following stopped by request
Follow generator cleanup complete
여기서는 제너레이터를 생성하고 해당 줄을 반복하기 시작합니다. 두 줄을 처리한 후 close 메서드를 사용하여 제너레이터를 명시적으로 닫습니다. 그러면 제너레이터는 GeneratorExit 예외를 처리하고 필요한 정리를 수행합니다.
다음으로 코루틴을 사용하여 간단한 데이터 처리 파이프라인을 만들겠습니다. 이 파이프라인은 다양한 단계에서 오류를 처리할 수 있습니다.
WebIDE 편집기를 열고 pipeline.py라는 새 파일을 만듭니다. 이 파일에 작성할 코드는 다음과 같습니다.
def consumer(func):
def start(*args,**kwargs):
c = func(*args,**kwargs)
next(c)
return c
return start
@consumer
def grep(pattern, target):
"""Filter lines containing pattern and send to target"""
try:
while True:
line = yield
if pattern in line:
target.send(line)
except Exception as e:
target.throw(e)
@consumer
def printer():
"""Print received items"""
try:
while True:
item = yield
print(f"PRINTER: {item}")
except Exception as e:
print(f"PRINTER ERROR: {repr(e)}")
def follow_and_process(filename, pattern):
"""Follow a file and process its contents"""
import time
import os
output = printer()
filter_pipe = grep(pattern, output)
try:
with open(filename, 'r') as f:
f.seek(0, os.SEEK_END)
while True:
line = f.readline()
if not line:
time.sleep(0.1)
continue
filter_pipe.send(line)
except KeyboardInterrupt:
print("Processing stopped by user")
finally:
filter_pipe.close()
output.close()
이 코드에서 consumer 데코레이터는 코루틴을 초기화하는 데 사용됩니다. grep 코루틴은 특정 패턴을 포함하는 줄을 필터링하여 다른 코루틴으로 보냅니다. printer 코루틴은 수신된 항목을 출력합니다. follow_and_process 함수는 파일을 읽고, grep 코루틴을 사용하여 해당 줄을 필터링하고, printer 코루틴을 사용하여 일치하는 줄을 출력합니다. 또한 KeyboardInterrupt 예외를 처리하고 적절한 정리를 보장합니다.
코드를 작성한 후 파일을 저장합니다.
데이터 처리 파이프라인을 테스트해 보겠습니다. 터미널에서 다음 명령을 실행합니다.
cd ~/project
python3 -c "from pipeline import follow_and_process; follow_and_process('stocklog.csv', 'IBM')"
다음과 유사한 출력이 표시됩니다.
PRINTER: "IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
PRINTER: "IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859
PRINTER: "IBM",102.95,"6/11/2007","09:39.44",-0.12,102.87,102.95,102.77,225350
이 출력은 파이프라인이 올바르게 작동하여 "IBM" 패턴을 포함하는 줄을 필터링하고 출력함을 보여줍니다.
프로세스를 중지하려면 Ctrl+C를 누릅니다. 다음 메시지가 표시됩니다.
Processing stopped by user
finally 블록은 제너레이터가 종료되는 방식에 관계없이 정리 작업이 수행되도록 합니다. 이는 프로그램의 무결성을 유지하고 리소스 누수를 방지하는 데 도움이 됩니다.이 랩에서는 Python 제너레이터와 코루틴에서 yield 문을 관리하기 위한 필수 기술을 배웠습니다. 클로저 또는 가비지 수집 중에 GeneratorExit 예외를 처리하고 반복 중단 및 재개를 제어하는 것을 포함하여 제너레이터 수명 관리에 대해 살펴보았습니다. 또한 throw() 메서드를 사용하고 예외를 적절하게 처리하기 위한 강력한 제너레이터를 작성하는 등 제너레이터에서 예외 처리에 대해 배웠습니다.
이러한 기술은 강력하고 유지 관리 가능한 Python 애플리케이션을 구축하는 데 필수적입니다. 데이터 처리, 비동기 작업 및 리소스 관리에 유용합니다. 제너레이터 수명을 적절하게 관리하고 예외를 처리함으로써 오류를 적절하게 처리하고 더 이상 필요하지 않은 경우 리소스를 정리하는 탄력적인 시스템을 만들 수 있습니다.