PEP 325 – 제너레이터의 리소스 해제 지원
- Author:
- Samuele Pedroni <pedronis at python.org>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 25-Aug-2003
- Python-Version:
- 2.4
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
제너레이터를 사용하면 데이터를 순회하는 코드를 자연스럽게 작성하고 추상화할 수 있습니다. 현재 적절한 시점에 해제해야 하는 외부 리소스가 관련된 경우, 제너레이터는 안타깝게도 충분하지 않습니다. 적절한 시점에 해제하기 위한 일반적인 관용구를 지원하지 않으므로, 제너레이터 내부의 try-finally 문에서 try 절에는 yield 문을 사용할 수 없습니다. finally 절의 실행은 보장할 수도 강제할 수도 없습니다.
이 PEP는 내장 제너레이터 타입이 close 메서드와 소멸 의미론을 구현하도록 제안하며, 이를 통해 yield 배치 제한을 해제하고 제너레이터의 적용 범위를 확장합니다.
선언
요청된 동작의 대부분을 더욱 개선된 형태로 포함하는 PEP 342를 채택하는 방향으로 거부되었습니다.
근거
Python 제너레이터를 사용하면 다양한 데이터 순회 시나리오를 자연스럽게 코딩할 수 있습니다. 제너레이터를 인스턴스화하면 이터레이터, 즉 순회를 추상화하는 일급 객체가 생성됩니다(일급 객체의 모든 장점을 갖습니다). 이러한 측면에서 제너레이터는 (Smalltalk 방식의) 블록을 받는 이터레이터 메서드를 사용하는 접근 방식과 강력함이 비슷하며, 몇 가지 장점도 제공합니다. 반면 현재의 제한 사항(제너레이터 내부의 try-finally 문에서 try 절에 yield를 사용할 수 없음)을 고려하면, 후자의 접근 방식이 순회뿐만 아니라 예외 처리와 적절한 리소스 획득 및 해제까지 캡슐화하는 데 더 적합해 보입니다.
예를 살펴보겠습니다(간단히 하기 위해 읽기 모드의 파일을 사용합니다).:
def all_lines(index_path):
for path in file(index_path, "r"):
for line in file(path.strip(), "r"):
yield line
이는 짧고 핵심적이지만, 파일을 적절한 시점에 닫기 위한 try-finally를 추가할 수 없습니다. (경로 대신 파일을 인자로 전달하고 파일을 닫는 책임을 호출자에게 맡길 수는 있지만, 인덱스의 내용에 따라 열리는 파일에는 동일한 방법을 적용할 수 없습니다.)
적절한 시점에 해제하려면 제너레이터만 사용하는 접근 방식의 단순성과 직접성을 포기해야 합니다(예를 들면 다음과 같습니다).
class AllLines:
def __init__(self, index_path):
self.index_path = index_path
self.index = None
self.document = None
def __iter__(self):
self.index = file(self.index_path, "r")
for path in self.index:
self.document = file(path.strip(), "r")
for line in self.document:
yield line
self.document.close()
self.document = None
def close(self):
if self.index:
self.index.close()
if self.document:
self.document.close()
다음과 같이 사용합니다.:
all_lines = AllLines("index.txt")
try:
for line in all_lines:
...
finally:
all_lines.close()
적절한 시점에 해제하는 더 복잡한 해법은 귀중한 단서를 제공하는 듯합니다. 우리가 한 일은 순회를 close 메서드가 있는 객체(이터레이터)로 캡슐화한 것입니다.
이 PEP는 제너레이터에 이러한 의미론을 갖는 close 메서드를 추가하여 예를 다음과 같이 다시 작성할 수 있도록 제안합니다.:
# Today this is not valid Python: yield is not allowed between
# try and finally, and generator type instances support no
# close method.
def all_lines(index_path):
index = file(index_path, "r")
try:
for path in index:
document = file(path.strip(), "r")
try:
for line in document:
yield line
finally:
document.close()
finally:
index.close()
all = all_lines("index.txt")
try:
for line in all:
...
finally:
all.close() # close on generator
현재 PEP 255는 try-finally 의미론에서 요구하는 finally 절의 실행을 보장할 수 없기 때문에 try-finally 문의 try 절 내부에서 yield를 사용하는 것을 허용하지 않습니다.
제안된 close 메서드의 의미론은 finally 절의 실행을 여전히 보장할 수는 없지만 필요한 경우 강제할 수 있도록 해야 합니다. 구체적으로 close 메서드의 동작은 제너레이터 프레임에서 반환을 강제하거나 제너레이터 프레임에 예외를 던져 제너레이터 내부의 finally 절이 실행되도록 해야 합니다. 적절한 시점에 리소스를 해제해야 하는 상황에서는 close를 명시적으로 호출할 수 있습니다.
한편 일반적인 경우에 최선의 노력을 다하는 정책을 구현하려면 제너레이터 소멸의 의미론을 확장해야 합니다. 구체적으로 소멸 시 close()를 호출해야 합니다. 최선의 노력만 가능한 이유는 애초에 소멸자의 실행 자체가 보장되지 않기 때문입니다.
이는 합리적인 절충안으로 보이며, 그 결과 전체적인 동작은 파일과 파일 닫기의 동작과 비슷해집니다.
가능한 의미론
내장 제너레이터 타입에는 close 메서드가 구현되어 있어야 하며, 이 메서드는 다음과 같이 호출할 수 있습니다.:
gen.close()
여기서 gen은 내장 제너레이터 타입의 인스턴스입니다. 제너레이터가 소멸될 때도 close 메서드의 동작이 호출되어야 합니다.
제너레이터가 이미 종료된 상태라면, close는 아무 동작도 하지 않아야 합니다.
그렇지 않은 경우, 반환 시맨틱스와 예외 시맨틱스라는 두 가지 대안 해법이 있습니다:
A - 반환 시맨틱스: 제너레이터를 재개하되, 재진입 지점의 명령이 반환문인 것처럼 제너레이터 실행이 계속되어야 합니다. 그 결과, 이후 허용될 try-yield-finally 패턴의 경우, 재진입 지점을 둘러싼 finally 절이 실행됩니다.
쟁점: close에 의한 강제 종료, 정상 종료, 제너레이터 또는 제너레이터가 호출한 코드로부터의 예외 전파를 구분할 수 있는 것이 중요합니까? 일반적인 경우에는 그렇지 않은 것으로 보이며, finally 절은 이 모든 경우에 동일하게 동작하도록 존재해야 하지만, 이 시맨틱스는 그러한 구분을 어렵게 만들 수 있습니다.
정상적인 반환에서와 마찬가지로 except 절은 실행되지 않는데, 레거시 제너레이터의 이러한 절들은 제너레이터 자체 또는 그로부터 호출된 코드에서 발생한 예외에 대해 실행되기를 기대합니다. close의 경우 이를 실행하지 않는 것이 옳아 보입니다.
B - 예외 시맨틱스: 제너레이터를 재개하되, 재진입 지점에서 특수 목적 예외(예: CloseGenerator)가 발생한 것처럼 실행이 계속되어야 합니다. close 구현은 이 예외를 소비해야 하며 더 이상 전파해서는 안 됩니다.
쟁점: 이 목적을 위해 StopIteration을 재사용해야 합니까? 아마 그렇지 않을 것입니다. 다른 제너레이터/이터레이터를 다루기 위해 StopIteration을 잡는 코드를 포함할 수 있는 레거시 제너레이터에 대해, close가 무해한 연산이기를 바랍니다.
일반적으로 예외 시맨틱스에서는, 제너레이터가 종료되지 않거나 특수 예외가 다시 전파되어 돌아오지 않을 경우 무엇을 해야 할지 불명확합니다. 그 밖의 다른 예외는 아마 전파되어야 하겠지만, 다음과 같은 레거시 제너레이터 코드의 가능성을 고려해 보십시오.:
try:
...
yield ...
...
except: # or except Exception:, etc
raise Exception("boom")
제너레이터가 yield 이후 일시 중단된 상태에서 close가 호출되면, except 절이 우리의 특수 목적 예외를 잡게 되어 다른 예외가 다시 전파되어 돌아오게 되는데, 이 경우에는 합리적으로 소비되어 무시되어야 하지만 일반적으로는 전파되어야 하므로, 이러한 시나리오들을 구분하기는 어려워 보입니다.
예외 접근 방식은 제너레이터가 종료 경우들을 구분하고 더 많은 제어권을 가질 수 있게 한다는 장점이 있습니다. 반면, 명확한 시맨틱스를 정의하기는 더 어려워 보입니다.
비고
이 제안이 채택된다면, 제너레이터가 자원을 획득하는지, 그래서 그 close 메서드가 호출되어야 하는지를 문서화하는 것이 일반적인 관행이 되어야 합니다. 제너레이터가 더 이상 사용되지 않는다면, close를 호출하는 것은 무해해야 합니다.
반면, 일반적인 시나리오에서는 제너레이터를 인스턴스화한 코드가 필요한 경우 close를 호출해야 합니다. 다른 곳에서 인스턴스화된 이터레이터/제너레이터를 다루는 범용 코드는 일반적으로 close 호출로 어지럽혀져서는 안 됩니다.
이터레이터, 제너레이터, 그리고 적시에 해제해야 할 자원을 획득하는 제너레이터 모두에 대한 소유권을 획득하여 이를 적절히 처리해야 하는 드문 경우의 코드는 쉽게 해결됩니다:
if hasattr(iterator, 'close'):
iterator.close()
미해결 쟁점
확정적인 시맨틱스가 선택되어야 합니다. 현재 귀도는 예외 시맨틱스를 선호합니다. 제너레이터가 종료하거나 특수 예외를 다시 전파하는 대신 값을 yield한다면, 제너레이터 측에서 특수 예외가 다시 발생되어야 합니다.
(가능한 시맨틱스에서 논의된 바와 같이) 잘못 변환된 특수 예외가 문제인지, 그리고 이에 대해 무엇을 해야 하는지는 여전히 불명확합니다.
구현 문제는 탐구해 볼 필요가 있습니다.
대안 아이디어
yield 배치 제한을 제거하고 제너레이터 소멸 시 finally 절 실행을 촉발해야 한다는 아이디어는 한 번 이상 제안된 바 있습니다. 이것만으로는 제너레이터가 획득한 자원의 시의적절한 해제를 보장할 수 없습니다.
PEP 288은 제너레이터에 사용자 정의 예외를 전달할 수 있도록 하는, 보다 일반적인 해법을 제안합니다. 이 PEP에서의 제안은 자원 해제 문제를 더 직접적으로 다룹니다. 만약 PEP 288이 구현된다면 close에 대한 예외 시맨틱스는 그 위에 계층화될 수 있지만, 반면 PEP 288은 더 일반적인 기능에 대해 별도의 사례를 만들어야 합니다.
Copyright
This document has been placed in the public domain.