Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 325 – 제너레이터의 자원 해제 지원

Author:
Samuele Pedroni <pedronis at python.org>
Status:
Rejected
Type:
Standards Track
Created:
25-Aug-2003
Python-Version:
2.4
Post-History:


Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

제너레이터를 사용하면 데이터 순회를 자연스럽게 코딩하고 추상화할 수 있습니다. 현재 적시에 적절히 해제해야 하는 외부 자원이 관련된 경우, 안타깝게도 제너레이터는 적합하지 않습니다. 적시 해제를 위한 전형적인 관용구가 지원되지 않습니다. 제너레이터 내부에서는 try-finally 문의 try 절에 yield 문을 사용할 수 없습니다. finally 절의 실행은 보장할 수도, 강제할 수도 없습니다.

이 PEP는 내장 제너레이터 타입이 close 메서드와 소멸 의미론을 구현하여 yield 배치에 대한 제약을 없애고 제너레이터의 적용 범위를 넓힐 것을 제안합니다.

결정

요청된 동작의 사실상 전부를 더 정제된 형태로 포함하는 PEP 342를 채택하면서 거부되었습니다.

근거

Python 제너레이터를 사용하면 많은 데이터 순회 시나리오를 자연스럽게 코딩할 수 있습니다. 제너레이터를 인스턴스화하면 이터레이터, 즉 순회를 추상화하는 일급 객체가 만들어집니다(일급 객체로서의 모든 장점을 가집니다). 이 점에서 제너레이터는 (스몰토크 방식의) 블록을 받는 이터레이터 메서드를 사용하는 접근 방식과 대등한 능력을 가지며, 그 방식에 비해 몇 가지 장점도 제공합니다. 반면 현재의 제약(제너레이터 내부의 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 문의 try 절 안에서 yield를 허용하지 않습니다. try-finally 의미론이 요구하는 대로 finally 절의 실행을 보장할 수 없기 때문입니다.

제안된 close 메서드의 의미론은 finally 절의 실행을 여전히 보장할 수는 없더라도 필요할 때 강제할 수는 있도록 해야 합니다. 구체적으로, close 메서드의 동작은 제너레이터 프레임에서 return을 강제하거나 그 안에서 예외를 발생시킴으로써 제너레이터 내부의 finally 절이 실행되도록 해야 합니다. 그러면 적시 자원 해제가 필요한 상황에서 close를 명시적으로 호출할 수 있습니다.

한편 일반적인 경우를 위한 최선 노력(best-effort) 정책을 구현하기 위해 제너레이터 소멸의 의미론을 확장해야 합니다. 구체적으로, 소멸 시에는 close()를 호출해야 합니다. 최선 노력이라는 한계는 애초에 소멸자의 실행 자체가 보장되지 않는다는 사실에서 비롯됩니다.

이는 합리적인 절충안으로 보이며, 그 결과 전체적인 동작은 파일과 파일 닫기의 동작과 유사해집니다.

가능한 의미론

내장 제너레이터 타입에는 close 메서드가 구현되어야 하며, 이 메서드는 다음과 같이 호출할 수 있습니다.:

gen.close()

여기서 gen은 내장 제너레이터 타입의 인스턴스입니다. 제너레이터가 소멸될 때에도 close 메서드의 동작이 호출되어야 합니다.

제너레이터가 이미 종료된 경우 close는 아무 동작도 하지 않아야 합니다.

그렇지 않은 경우에는 반환 의미론과 예외 의미론이라는 두 가지 대안이 있습니다:

A - 반환 의미: 제너레이터는 재개되어야 하며, 제너레이터 실행은 재진입점의 명령이 return인 것처럼 계속되어야 합니다. 따라서 그때 허용될 try-yield-finally 패턴의 경우, 재진입점을 둘러싼 finally 절이 실행됩니다.

쟁점: close에 의한 강제 종료, 정상 종료, 그리고 제너레이터나 제너레이터가 호출한 코드에서 비롯된 예외 전파를 서로 구별할 수 있는 것이 중요합니까? 일반적인 경우에는 그렇지 않은 것으로 보이며, finally 절은 이 모든 경우에 똑같이 동작하도록 존재해야 합니다. 그렇더라도 이 의미론에서는 그러한 구별이 어려울 수 있습니다.

일반적인 return의 경우와 마찬가지로 except 절은 실행되지 않습니다. 기존 제너레이터의 이러한 절은 제너레이터 자신이나 제너레이터가 호출한 코드에서 발생한 예외에 대해 실행될 것을 전제로 합니다. close의 경우 이 절들을 실행하지 않는 것이 올바른 것으로 보입니다.

B - 예외 의미: 제너레이터는 재개되어야 하며, 실행은 재진입점에서 특수 목적의 예외(예: CloseGenerator)가 발생한 것처럼 계속되어야 합니다. close 구현은 이 예외를 소비해야 하며 더 이상 전파하지 않아야 합니다.

쟁점: 이 목적으로 StopIteration을 재사용해야 합니까? 아마 그렇지 않을 것입니다. close는 기존 제너레이터에 해가 없는 연산이어야 합니다. 기존 제너레이터에는 다른 제너레이터/이터레이터를 다루기 위해 StopIteration을 잡는 코드가 들어 있을 수 있기 때문입니다.

일반적으로 예외 의미론에서는 제너레이터가 종료되지 않거나 특수 예외가 다시 전파되어 돌아오지 않을 때 어떻게 해야 할지가 불분명합니다. 그 밖의 다른 예외는 아마 전파되어야 하겠지만, 다음과 같은 기존 제너레이터 코드가 있을 수 있다는 점을 고려해 보십시오:

try:
    ...
    yield ...
    ...
except: # or except Exception:, etc
    raise Exception("boom")

제너레이터가 yield 이후에 일시 중단된 상태에서 close가 호출되면, except 절이 우리의 특수 목적 예외를 잡게 되므로 다른 예외가 전파되어 돌아옵니다. 이 경우에는 그 예외를 적절히 소비하고 무시해야 마땅하지만 일반적으로는 전파해야 하는데, 이 두 시나리오를 구분하기는 어려워 보입니다.

예외 방식은 제너레이터가 여러 종료 상황을 서로 구별하고 더 많이 제어할 수 있게 해 준다는 장점이 있습니다. 반면에 명확한 의미론을 정의하기는 더 어려워 보입니다.

참고 사항

이 제안이 수락되면 제너레이터가 자원을 획득하는지, 그래서 그 close 메서드를 호출해야 하는지를 문서화하는 것이 일반적인 관행이 되어야 합니다. 더 이상 사용하지 않는 제너레이터라면 close를 호출해도 해가 없어야 합니다.

한편, 일반적인 시나리오에서는 제너레이터를 인스턴스화한 코드가 필요한 경우 close를 호출해야 합니다. 다른 곳에서 인스턴스화된 이터레이터/제너레이터를 다루는 범용 코드는 일반적으로 close 호출로 어지럽혀져서는 안 됩니다.

이터레이터, 제너레이터, 그리고 적시에 해제해야 하는 자원을 획득하는 제너레이터의 소유권을 넘겨받아 이들 모두를 적절히 처리해야 하는 드문 경우의 코드는 쉽게 해결됩니다:

if hasattr(iterator, 'close'):
    iterator.close()

미해결 문제

확정적인 의미론을 선택해야 합니다. 현재 Guido는 예외 의미론을 선호합니다. 제너레이터가 종료하거나 특수 예외를 다시 전파하는 대신 값을 yield하면, 제너레이터 쪽에서 특수 예외를 다시 발생시켜야 합니다.

(가능한 의미론에서 논의한 것처럼) 잘못 변환된 특수 예외가 문제가 되는지, 그리고 이를 어떻게 처리해야 하는지는 아직 불분명합니다.

구현상의 문제를 검토해야 합니다.

대안 아이디어

yield 배치 제한을 없애고 제너레이터가 소멸될 때 finally 절이 실행되도록 해야 한다는 아이디어는 여러 차례 제안되었습니다. 그러나 이것만으로는 제너레이터가 획득한 자원을 적시에 해제하도록 강제할 수 있다고 보장하지 못합니다.

PEP 288은 제너레이터에 사용자 정의 예외를 전달할 수 있게 하는 더 일반적인 해결책을 제안합니다. 이 PEP의 제안은 자원 해제 문제를 더 직접적으로 다룹니다. PEP 288이 구현된다면 close에 대한 예외 의미를 그 위에 얹을 수 있습니다. 반면 PEP 288은 더 일반적인 기능에 대해 별도의 근거를 제시해야 합니다.