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

Python 개선 제안 한국어 번역

PEP 554 – 표준 라이브러리의 여러 인터프리터

Author:
Eric Snow <ericsnowcurrently at gmail.com>
Discussions-To:
Discourse thread
Status:
Superseded
Type:
Standards Track
Created:
05-Sep-2017
Python-Version:
3.13
Post-History:
07-Sep-2017, 08-Sep-2017, 13-Sep-2017, 05-Dec-2017, 04-May-2020, 14-Mar-2023, 01-Nov-2023
Superseded-By:
734

Table of Contents

번역·라이선스 안내

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

Note

이 PEP는 사실상 PEP 734에서 더 깔끔한 형태로 이어집니다. 이 PEP는 PEP 734에서 제거된 여러 배경 정보 섹션과 보류/거부된 아이디어 섹션을 보존하기 위해 현재 형태 그대로 유지됩니다.

초록

CPython은 버전 1.5(1997)부터 같은 프로세스 안에서 여러 인터프리터(“서브인터프리터”라고도 함)를 지원해 왔습니다. 이 기능은 C-API를 통해 사용할 수 있었습니다. [c-api] 여러 인터프리터는 서로 상대적으로 격리된 상태로 동작하며, 이는 동시성에 대한 새로운 대안적 접근 방식을 가능하게 합니다.

이 제안은 표준 라이브러리 interpreters 모듈을 도입합니다. 이 모듈은 C-API가 이미 제공하는 여러 인터프리터의 기본 기능을 노출하며, 인터프리터 간 통신에 대한 기본 지원도 함께 제공합니다. PEP 684가 Python 3.12에서 인터프리터별 GIL을 도입했으므로 이 모듈은 특히 의미가 있습니다.

제안

요약:

  • 새로운 표준 라이브러리 모듈 추가: “interpreters”
  • concurrent.futures.InterpreterPoolExecutor 추가
  • 확장 모듈 유지 관리자를 위한 지원

“interpreters” 모듈

interpreters 모듈은 여러 인터프리터 기능에 대한 고수준 인터페이스를 제공하고, 새로운 저수준 _interpreters를 감쌉니다(threading 모듈과 같은 방식입니다). 구체적인 사용법과 사용 사례는 Examples 섹션을 참조하십시오.

이 모듈은 (CPython에) 이미 존재하는 여러 인터프리터 지원을 노출할 뿐만 아니라, 인터프리터 간에 데이터를 전달하는 기본 메커니즘도 지원합니다. 여기에는 대상 서브인터프리터의 __main__ 모듈에 “공유 가능한” 객체를 설정하는 작업이 포함됩니다. os.pipe()와 같은 일부 객체는 추가적인 통신에 사용될 수 있습니다. 이 모듈은 또한 인터프리터 간 통신을 보여 주는 예시로 “채널”의 최소 구현을 제공합니다.

객체는 자신이 생성된 인터프리터에 묶여 있으므로 인터프리터 간에 공유되지 않는다는 점에 유의하십시오. 대신 객체의 데이터가 인터프리터 간에 전달됩니다. 인터프리터 간 공유/통신에 대한 자세한 내용은 Shared Data 및 API For Communication 섹션을 참조하십시오.

interpreters 모듈의 API 요약

다음은 interpreters 모듈의 API 요약입니다. 제안된 클래스와 함수에 대한 더 자세한 설명은 아래의 “interpreters” Module API 섹션을 참조하십시오.

인터프리터 생성 및 사용:

시그니처 설명
list_all() -> [Interpreter] 존재하는 모든 인터프리터를 가져옵니다.
get_current() -> Interpreter 현재 실행 중인 인터프리터를 가져옵니다.
get_main() -> Interpreter 메인 인터프리터를 가져옵니다.
create() -> Interpreter 새로운 (유휴 상태의) Python 인터프리터를 초기화합니다.

시그니처 설명
class Interpreter 단일 인터프리터입니다.
.id 인터프리터의 ID입니다(읽기 전용).
.is_running() -> bool 인터프리터가 현재 코드를 실행하고 있습니까?
.close() 인터프리터를 마무리하고 파괴합니다.
.set_main_attrs(**kwargs) __main__에 “공유 가능한” 객체를 바인딩합니다.
.get_main_attr(name) __main__에서 “공유 가능한” 객체를 가져옵니다.
.exec(src_str, /)
주어진 소스 코드를 인터프리터에서 (현재
스레드에서) 실행합니다.

인터프리터 간 통신을 위한 항목:

시그니처 설명
is_shareable(obj) -> Bool
객체의 데이터를 인터프리터 간에 전달할
수 있습니까?
create_channel() -> (RecvChannel, SendChannel)
인터프리터 간에 데이터를 전달하기 위한
새 채널을 생성합니다.

concurrent.futures.InterpreterPoolExecutor

ThreadPoolExecutor를 확장하여 스레드별 작업을 서브인터프리터에서 실행하는 실행기(executor)가 추가됩니다. 처음에는 Interpreter.exec()가 받는 것(예: str 스크립트)만 작업으로 지원합니다. 그러나 번거로움을 줄이기 위해 일부 함수도 지원할 수 있으며, 결국에는 작업과 인자를 피클링하는 별도의 메서드도 지원할 수 있습니다(대신 짧게 실행되는 작업에서는 성능이 희생됩니다).

확장 모듈 유지 관리자를 위한 지원

실제로는 다단계 초기화(PEP 489)를 구현하는 확장은 격리된 것으로 간주되므로 여러 인터프리터와 호환됩니다. 그렇지 않으면 “호환되지 않는” 것으로 간주됩니다.

많은 확장 모듈이 아직 호환되지 않습니다. 그러한 확장 모듈이 여러 인터프리터를 지원하도록 갱신되면 유지 관리자와 사용자 모두 이득을 봅니다. 그동안 사용자는 여러 인터프리터를 사용할 때 발생하는 실패로 혼란을 겪을 수 있으며, 이는 확장 유지 관리자에게 부정적인 영향을 줄 수 있습니다. 아래의 Concerns를 참조하십시오.

그 영향을 완화하고 호환성 확보를 앞당기기 위해 다음과 같이 합니다:

  • 확장 모듈이 여러 인터프리터에서의 사용을 지원할 필요가 없다는 점을 분명히 합니다
  • 호환되지 않는 모듈을 서브인터프리터에서 임포트하면 ImportError를 발생시킵니다
  • 유지 관리자가 호환성을 확보하도록 돕는 자료(예: 문서)를 제공합니다
  • Cython과 (PyPI에서) 가장 많이 사용되는 확장 모듈의 유지 관리자에게 연락하여 피드백을 받고, 가능하면 도움을 제공합니다

예시

현재 OS 스레드에서 격리된 코드 실행하기

interp = interpreters.create()
print('before')
interp.exec('print("during")')
print('after')

다른 스레드에서 실행하기

interp = interpreters.create()
def run():
    interp.exec('print("during")')
t = threading.Thread(target=run)
print('before')
t.start()
t.join()
print('after')

인터프리터 미리 채우기

interp = interpreters.create()
interp.exec(tw.dedent("""
    import some_lib
    import an_expensive_module
    some_lib.set_up()
    """))
wait_for_request()
interp.exec(tw.dedent("""
    some_lib.handle_request()
    """))

예외 처리하기

interp = interpreters.create()
try:
    interp.exec(tw.dedent("""
        raise KeyError
        """))
except interpreters.RunFailedError as exc:
    print(f"got the error from the subinterpreter: {exc}")

예외 다시 발생시키기

interp = interpreters.create()
try:
    try:
        interp.exec(tw.dedent("""
            raise KeyError
            """))
    except interpreters.RunFailedError as exc:
        raise exc.__cause__
except KeyError:
    print("got a KeyError from the subinterpreter")

이 패턴은 나중에 개선할 후보라는 점에 유의하십시오.

__main__ 네임스페이스와 상호 작용하기

interp = interpreters.create()
interp.set_main_attrs(a=1, b=2)
interp.exec(tw.dedent("""
    res = do_something(a, b)
    """))
res = interp.get_main_attr('res')

OS 파이프를 사용하여 동기화하기

interp = interpreters.create()
r1, s1 = os.pipe()
r2, s2 = os.pipe()

def task():
    interp.exec(tw.dedent(f"""
        import os
        os.read({r1}, 1)
        print('during B')
        os.write({s2}, '')
        """))

t = threading.thread(target=task)
t.start()
print('before')
os.write(s1, '')
print('during A')
os.read(r2, 1)
print('after')
t.join()

파일 디스크립터 공유하기

interp = interpreters.create()
with open('spamspamspam') as infile:
    interp.set_main_attrs(fd=infile.fileno())
    interp.exec(tw.dedent(f"""
        import os
        for line in os.fdopen(fd):
            print(line)
        """))

pickle을 통해 객체 전달하기

interp = interpreters.create()
r, s = os.pipe()
interp.exec(tw.dedent(f"""
    import os
    import pickle
    reader = {r}
    """))
interp.exec(tw.dedent("""
        data = b''
        c = os.read(reader, 1)
        while c != b'\x00':
            while c != b'\x00':
                data += c
                c = os.read(reader, 1)
            obj = pickle.loads(data)
            do_something(obj)
            c = os.read(reader, 1)
        """))
for obj in input:
    data = pickle.dumps(obj)
    os.write(s, data)
    os.write(s, b'\x00')
os.write(s, b'\x00')

인터프리터의 stdout 캡처하기

interp = interpreters.create()
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
    interp.exec(tw.dedent("""
        print('spam!')
        """))
assert(stdout.getvalue() == 'spam!')

# alternately:
interp.exec(tw.dedent("""
    import contextlib, io
    stdout = io.StringIO()
    with contextlib.redirect_stdout(stdout):
        print('spam!')
    captured = stdout.getvalue()
    """))
captured = interp.get_main_attr('captured')
assert(captured == 'spam!')

파이프(os.pipe())도 비슷하게 사용할 수 있습니다.

모듈 실행하기

interp = interpreters.create()
main_module = mod_name
interp.exec(f'import runpy; runpy.run_module({main_module!r})')

스크립트로 실행하기(zip 아카이브와 디렉터리 포함)

interp = interpreters.create()
main_script = path_name
interp.exec(f"import runpy; runpy.run_path({main_script!r})")

채널을 사용하여 통신하기

tasks_recv, tasks = interpreters.create_channel()
results, results_send = interpreters.create_channel()

def worker():
    interp = interpreters.create()
    interp.set_main_attrs(tasks=tasks_recv, results=results_send)
    interp.exec(tw.dedent("""
        def handle_request(req):
            ...

        def capture_exception(exc):
            ...

        while True:
            try:
                req = tasks.recv()
            except Exception:
                # channel closed
                break
            try:
                res = handle_request(req)
            except Exception as exc:
                res = capture_exception(exc)
            results.send_nowait(res)
        """))
threads = [threading.Thread(target=worker) for _ in range(20)]
for t in threads:
    t.start()

requests = ...
for req in requests:
    tasks.send(req)
tasks.close()

for t in threads:
    t.join()

memoryview 공유하기(맵리듀스를 떠올려 보십시오)

data, chunksize = read_large_data_set()
buf = memoryview(data)
numchunks = (len(buf) + 1) / chunksize
results = memoryview(b'\0' * numchunks)

tasks_recv, tasks = interpreters.create_channel()

def worker():
    interp = interpreters.create()
    interp.set_main_attrs(data=buf, results=results, tasks=tasks_recv)
    interp.exec(tw.dedent("""
        while True:
            try:
                req = tasks.recv()
            except Exception:
                # channel closed
                break
            resindex, start, end = req
            chunk = data[start: end]
            res = reduce_chunk(chunk)
            results[resindex] = res
        """))
t = threading.Thread(target=worker)
t.start()

for i in range(numchunks):
    if not workers_running():
        raise ...
    start = i * chunksize
    end = start + chunksize
    if end > len(buf):
        end = len(buf)
    tasks.send((start, end, i))
tasks.close()
t.join()

use_results(results)

근거

여러 인터프리터에서 코드를 실행하면 같은 프로세스 안에서도 유용한 수준의 격리를 얻을 수 있습니다. 이는 여러 가지 방식으로 활용할 수 있습니다. 또한 서브인터프리터는 이러한 격리를 확장할 수 있는 잘 정의된 프레임워크를 제공합니다. (PEP 684를 참조하십시오.)

Alyssa (Nick) Coghlan은 멀티프로세싱과 비교하여 몇 가지 이점을 설명했습니다 [benefits]:

[I] expect that communicating between subinterpreters is going
to end up looking an awful lot like communicating between
subprocesses via shared memory.

The trade-off between the two models will then be that one still
just looks like a single process from the point of view of the
outside world, and hence doesn't place any extra demands on the
underlying OS beyond those required to run CPython with a single
interpreter, while the other gives much stricter isolation
(including isolating C globals in extension modules), but also
demands much more from the OS when it comes to its IPC
capabilities.

The security risk profiles of the two approaches will also be quite
different, since using subinterpreters won't require deliberately
poking holes in the process isolation that operating systems give
you by default.

CPython은 버전 1.5부터 여러 인터프리터를 지원해 왔으며, 그 지원 수준은 점점 높아져 왔습니다. 이 기능은 강력한 도구가 될 잠재력이 있지만, 여러 인터프리터 기능을 Python에서 직접 쉽게 사용할 수 없었기 때문에 방치되어 왔습니다. 기존 기능을 표준 라이브러리로 노출하면 이러한 상황을 되돌리는 데 도움이 될 것입니다.

이 제안은 같은 Python 프로세스 안에서 서로 격리된 여러 인터프리터라는 기본 기능을 사용할 수 있게 하는 데 초점을 둡니다. 이는 Python에게 새로운 영역이므로, 인터프리터와 함께 제공할 최선의 도구가 무엇인지는 상대적으로 불확실합니다. 따라서 이 제안에서 추가하는 기능은 가능한 한 최소화합니다.

우려 사항

  • “서브인터프리터는 그만한 수고를 들일 가치가 없습니다”

일부에서는 서브인터프리터가 Python의 공식 기능으로 만들 만큼 충분한 이점을 주지 않는다고 주장해 왔습니다. 언어(또는 표준 라이브러리)에 기능을 추가하면 언어의 크기가 커진다는 비용이 따릅니다. 따라서 추가되는 기능은 그 비용을 감당할 만한 가치가 있어야 합니다.

이 경우, 여러 인터프리터 지원은 격리된 실행 스레드에 초점을 둔 새로운 동시성 모델을 제공합니다. 또한 여러 인터프리터는 여러 CPU 코어를 동시에 사용할 수 있게 하는 CPython 변경의 기회를 제공합니다(현재는 GIL 때문에 불가능합니다. PEP 684를 참조하십시오).

서브인터프리터의 대안으로는 스레딩, async, 멀티프로세싱이 있습니다. 스레딩은 GIL로 인해 제약을 받으며, async는 모든 문제에(그리고 모든 사람에게) 알맞은 해결책이 아닙니다. 멀티프로세싱 역시 일부 상황에서는 유용하지만 모든 상황에서 그런 것은 아닙니다. (multiprocessing 모듈을 거치지 않는) 직접 IPC도 비슷한 이점을 제공하지만 같은 한계가 있습니다.

특히 서브인터프리터는 위의 어떤 것도 대체하려는 것이 아닙니다. 분명 일부 영역에서는 겹치지만, 서브인터프리터의 이점으로는 격리와 (잠재적인) 성능이 있습니다. 특히 서브인터프리터는 다른 곳에서 성공을 거두었고 일부 Python 사용자에게 매력적일 대안 동시성 모델(예: CSP)로 가는 직접적인 경로를 제공합니다. 이것이 바로 interpreters 모듈이 제공할 핵심 가치입니다.

  • “여러 인터프리터에 대한 표준 라이브러리 지원은 C 확장 작성자에게 추가 부담을 줍니다”

아래의 Interpreter Isolation 섹션에서는 CPython 서브인터프리터의 격리가 불완전한 부분들을 짚어 봅니다. 가장 주목할 만한 것은 내부 상태를 저장하기 위해 C 전역 변수를 사용하는 확장 모듈입니다. (PEP 3121과 PEP 489가 이 문제에 대한 해결책을 제공하며, 이어서 효율성을 개선하는 몇 가지 추가 API도 나왔습니다. 예: PEP 573).

결과적으로, 확장 모듈을 배포하는 프로젝트는 사용자들이 서브인터프리터를 사용하기 시작하면서 유지 보수 부담이 늘어날 수 있으며, 이 경우 해당 모듈이 제대로 동작하지 않을 수 있습니다. 이러한 상황은 내부 상태를 저장하기 위해 C 전역 변수를 사용하는(또는 C 전역 변수를 사용하는 라이브러리를 쓰는) 모듈에 한정됩니다. numpy의 경우, 보고된 버그 발생률은 6개월에 한 건입니다. [bug-rate]

결국 이것은 실제로 얼마나 자주 문제가 될 것인가의 문제로 귀결됩니다. 즉, 얼마나 많은 프로젝트가 영향을 받을지, 그 사용자들이 얼마나 자주 영향을 받을지, 프로젝트에 추가되는 유지 보수 부담이 얼마나 될지, 그리고 그러한 비용을 상쇄할 만한 서브인터프리터의 전반적인 이점이 무엇인지의 문제입니다. 이 PEP의 입장은 실제 추가 유지 보수 부담이 작을 것이며, 서브인터프리터가 그만한 가치를 가지게 되는 임계점보다 훨씬 낮을 것이라는 것입니다.

  • “새로운 동시성 API를 만드는 일은 훨씬 더 많은 숙고와 실험이 필요하므로, 새 모듈을 곧바로 표준 라이브러리에 넣어서는 안 되며, 어쩌면 영영 넣지 않아야 할 수도 있습니다”

asyncio의 경우처럼 새로운 동시성 모델을 위한 API를 도입하는 일은 많은 신중한 검토가 필요한 매우 큰 프로젝트입니다. 이는 이 PEP가 제안하는 것처럼 간단하게 할 수 있는 일이 아니며, 성숙해지기까지 PyPI에서 상당한 시간을 보내야 할 가능성이 큽니다. (python-dev에 올라온 Nathaniel의 글을 참고하십시오.)

그러나 이 PEP는 어떠한 새로운 동시성 API도 제안하지 않습니다. 기껏해야 (Python에서는 비교적) 새로운 동시성 모델과 관련된 패턴을 따르는 코드를 작성하는 데 사용할 수 있는 최소한의 도구(예: 서브인터프리터, 채널)를 노출할 뿐입니다. 이러한 도구는 그러한 동시성 모델을 위한 API의 기반으로도 사용될 수 있습니다. 다시 말하지만, 이 PEP는 그러한 API를 제안하지 않습니다.

  • “서브인터프리터들이 여전히 GIL을 공유한다면 서브인터프리터를 노출할 의미가 없습니다”
  • “GIL을 인터프리터별로 만드는 작업은 큰 혼란을 일으키며 위험합니다”

흔한 오해 중 하나는 이 PEP가 인터프리터들이 더 이상 GIL을 공유하지 않을 것이라는 약속까지 포함하고 있다는 것입니다. 이 점을 분명히 하고 나면, 다음 질문은 “그렇다면 무슨 의미가 있습니까?”입니다. 이에 대해서는 이 PEP에서 이미 자세히 답하고 있습니다. 분명히 말씀드리면, 그 가치는 다음에 있습니다:

* increase exposure of the existing feature, which helps improve
  the code health of the entire CPython runtime
* expose the (mostly) isolated execution of interpreters
* preparation for per-interpreter GIL
* encourage experimentation
  • “데이터 공유는 멀티 코어 환경에서 캐시 성능에 부정적인 영향을 줄 수 있습니다”

(캐시 라인 핑퐁에 대해서는 [cache-line-ping-pong]을 참조하십시오.)

인터프리터 간에 데이터를 실제로 공유할 당장의 계획은 없고 대신 복사에 집중하고 있으므로, 당분간은 이것이 문제가 되지 않을 것입니다.

서브인터프리터에 대하여

동시성

동시성은 소프트웨어 개발에서 까다로운 분야입니다. 수십 년에 걸친 연구와 실무를 통해 각기 다른 목표를 가진 매우 다양한 동시성 모델이 등장했습니다. 대부분은 정확성과 사용성에 중점을 둡니다.

동시성 모델의 한 부류는 어떤 메시지 전달 방식을 통해 상호 작용하는, 격리된 실행 스레드에 초점을 맞춥니다. 대표적인 예로 Communicating Sequential Processes [CSP]가 있습니다(Go의 동시성은 대략 이를 기반으로 합니다). CPython 인터프리터에 내재된 의도적인 격리 덕분에 인터프리터는 이 접근 방식에 잘 들어맞습니다.

공유 데이터

CPython의 인터프리터는 스레드와 달리 본질적으로 격리되어 있습니다(아래에서 설명하는 단서가 있습니다). 따라서 공유 메모리를 통해 통신하는 동일한 접근 방식은 통하지 않습니다. 대안이 없다면, 여러 인터프리터를 통한 동시성의 효과적인 활용은 크게 제한됩니다.

여기서 핵심 과제는 객체 소유권, 가시성, 가변성에 대한 여러 제약 때문에 인터프리터 간 객체 공유가 복잡해진다는 점입니다. 개념적인 수준에서는 객체가 한 번에 하나의 인터프리터에만 존재할 때 동시성을 추론하기가 더 쉽습니다. 기술적인 수준에서는 CPython의 현재 메모리 모델 때문에 Python 객체를 인터프리터 간에 안전하게 공유하는 방식이 제한됩니다. 사실상 객체는 자신이 생성된 인터프리터에 묶여 있습니다. 게다가 인터프리터가 더 격리될수록(예: GIL 제거 이후) 객체 공유의 복잡성은 커집니다(다만 일부 “불멸(immortal)” 객체에 대해서는 이 문제가 어느 정도 완화됩니다(PEP 683 참조).

따라서 공유 메커니즘은 신중하게 검토해야 합니다. 타당한 해결책은 여러 가지가 있으며, 그중 몇몇은 Python의 표준 라이브러리와 C-API에서 지원하기에 적합할 수 있습니다. 그러한 해결책은 어느 것이든 다른 해결책과 많은 특성을 공유할 가능성이 큽니다.

그동안 여기서는 객체를 공유하는 방식에 대해 어느 정도 선례를 남기는 최소한의 해결책(Interpreter.set_main_attrs())을 제안합니다. 더 중요한 점은 이 해결책이 나중에 더 발전된 방식을 도입하기 쉽게 해 주고, 그러한 방식들이 공존하며 협력할 수 있게 해 준다는 것입니다. 이를 보여 주려는 목적도 있어, 조금 더 발전된 공유 해결책으로서 “채널(channels)”의 기본 구현을 제공할 것입니다.

별도의 제안에서는 다음을 다룰 수 있습니다:

  • Interpreter.set_main_attrs() 구현에 기반한 공개 C-API 추가
  • “interpreters” 모듈에 다른 공유 방식 추가

통신을 가능하게 하는 근본적인 특징은 대부분의 객체를 기반 원시 데이터의 어떤 인코딩으로 변환할 수 있으며, 그 인코딩은 인터프리터 간에 안전하게 전달할 수 있다는 점입니다. 예를 들어 int 객체는 C long 값으로 변환되어 다른 인터프리터로 전송된 다음, 그곳에서 다시 int 객체로 변환될 수 있습니다. 또 다른 예로, None은 있는 그대로 전달할 수 있습니다.

어쨌든 여기서 최선의 방향을 정하는 작업은 대부분 이 PEP의 범위를 벗어납니다. 그동안 이 제안에서는 파이프(os.pipe())를 사용하는 기본적인 임시 해결책을 설명하고, 전용 기능(“채널”)도 제공합니다. 아래의 API For Communication을 참조하십시오.

인터프리터 격리

CPython의 인터프리터는 서로 엄격하게 격리되도록 의도되어 있습니다. 각 인터프리터는 모든 모듈, 클래스, 함수, 변수의 자체 사본을 가집니다. 확장 모듈을 포함해 C 수준의 상태에도 마찬가지로 적용됩니다. 자세한 내용은 CPython C-API 문서에서 설명합니다. [caveats]

하지만 인터프리터가 일부 상태를 실제로 공유하는 경우도 있습니다. 우선 일부 프로세스 전역 상태는 여전히 공유됩니다:

  • 파일 디스크립터
  • 저수준 환경 변수
  • 프로세스 메모리(다만 할당자는 격리되어 있습니다)
  • 내장 타입(예: dict, bytes)
  • 싱글턴(예: None)
  • 내장/확장/동결 모듈의 기반 정적 모듈 데이터(예: 함수)

이를 변경할 계획은 없습니다.

둘째, 버그 때문이거나 서브인터프리터를 고려하지 않은 구현 때문에 일부 격리에 결함이 있습니다. 여기에는 C 전역 변수에 의존하는 확장 모듈 같은 것들이 포함됩니다. [cryptography] 이러한 경우에는 버그를 등록해야 합니다(일부는 이미 등록되어 있습니다):

마지막으로, CPython의 현재 설계 때문에 가능한 격리 중 일부가 빠져 있습니다. 현재 이 영역의 공백을 해결하기 위한 개선 작업이 진행되고 있습니다:

  • PyGILState_* API를 사용하는 확장은 다소 호환되지 않습니다 [gilstate]

기존 사용 사례

여러 인터프리터 지원은 널리 사용되는 기능이 아니었습니다. 실제로 널리 사용된 사례로 문서화된 것은 mod_wsgi, OpenStack Ceph, JEP 등 극소수에 불과합니다. 한편으로 이러한 사례는 기존의 여러 인터프리터 지원이 비교적 안정적이라는 확신을 줍니다. 다른 한편으로는 이 기능의 유용성을 판단할 만한 표본의 크기가 충분하지 않습니다.

대체 Python 구현

저는 서브인터프리터 지원에 관해 여러 Python 구현자에게 의견을 구했습니다. 각 프로젝트는 (원한다면) 같은 프로세스 안에서 여러 인터프리터를 큰 어려움 없이 지원할 수 있을 것이라고 밝혔습니다. 제가 연락한 프로젝트는 다음과 같습니다:

  • jython ([jython])
  • ironpython (개인 서신)
  • pypy (개인 서신)
  • micropython (개인 서신)

“interpreters” 모듈 API

이 모듈은 다음 함수를 제공합니다:

list_all() -> [Interpreter]

   Return a list of all existing interpreters.

get_current() => Interpreter

   Return the currently running interpreter.

get_main() => Interpreter

   Return the main interpreter.  If the Python implementation
   has no concept of a main interpreter then return None.

create() -> Interpreter

   Initialize a new Python interpreter and return it.
   It will remain idle until something is run in it and always
   run in its own thread.

is_shareable(obj) -> bool:

   Return True if the object may be "shared" between interpreters.
   This does not necessarily mean that the actual objects will be
   shared.  Instead, it means that the objects' underlying data will
   be shared in a cross-interpreter way, whether via a proxy, a
   copy, or some other means.

이 모듈은 다음 클래스도 제공합니다:

class Interpreter(id):

   id -> int:

      The interpreter's ID. (read-only)

   is_running() -> bool:

      Return whether or not the interpreter's "exec()" is currently
      executing code.  Code running in subthreads is ignored.
      Calling this on the current interpreter will always return True.

   close():

      Finalize and destroy the interpreter.

      This may not be called on an already running interpreter.
      Doing so results in a RuntimeError.

   set_main_attrs(iterable_or_mapping, /):
   set_main_attrs(**kwargs):

      Set attributes in the interpreter's __main__ module
      corresponding to the given name-value pairs.  Each value
      must be a "shareable" object and will be converted to a new
      object (e.g. copy, proxy) in whatever way that object's type
      defines.  If an attribute with the same name is already set,
      it will be overwritten.

      This method is helpful for setting up an interpreter before
      calling exec().

   get_main_attr(name, default=None, /):

      Return the value of the corresponding attribute of the
      interpreter's __main__ module.  If the attribute isn't set
      then the default is returned.  If it is set, but the value
      isn't "shareable" then a ValueError is raised.

      This may be used to introspect the __main__ module, as well
      as a very basic mechanism for "returning" one or more results
      from Interpreter.exec().

   exec(source_str, /):

      Run the provided Python source code in the interpreter,
      in its __main__ module.

      This may not be called on an already running interpreter.
      Doing so results in a RuntimeError.

      An "interp.exec()" call is similar to a builtin exec() call
      (or to calling a function that returns None).  Once
      "interp.exec()" completes, the code that called "exec()"
      continues executing (in the original interpreter).  Likewise,
      if there is any uncaught exception then it effectively
      (see below) propagates into the code where ``interp.exec()``
      was called.  Like exec() (and threads), but unlike function
      calls, there is no return value.  If any "return" value from
      the code is needed, send the data out via a pipe (os.pipe())
      or channel or other cross-interpreter communication mechanism.

      The big difference from exec() or functions is that
      "interp.exec()" executes the code in an entirely different
      interpreter, with entirely separate state.  The interpreters
      are completely isolated from each other, so the state of the
      original interpreter (including the code it was executing in
      the current OS thread) does not affect the state of the target
      interpreter (the one that will execute the code).  Likewise,
      the target does not affect the original, nor any of its other
      threads.

      Instead, the state of the original interpreter (for this thread)
      is frozen, and the code it's executing code completely blocks.
      At that point, the target interpreter is given control of the
      OS thread.  Then, when it finishes executing, the original
      interpreter gets control back and continues executing.

      So calling "interp.exec()" will effectively cause the current
      Python thread to completely pause.  Sometimes you won't want
      that pause, in which case you should make the "exec()" call in
      another thread.  To do so, add a function that calls
      "interp.exec()" and then run that function in a normal
      "threading.Thread".

      Note that the interpreter's state is never reset, neither
      before "interp.exec()" executes the code nor after.  Thus the
      interpreter state is preserved between calls to
      "interp.exec()".  This includes "sys.modules", the "builtins"
      module, and the internal state of C extension modules.

      Also note that "interp.exec()" executes in the namespace of the
      "__main__" module, just like scripts, the REPL, "-m", and
      "-c".  Just as the interpreter's state is not ever reset, the
      "__main__" module is never reset.  You can imagine
      concatenating the code from each "interp.exec()" call into one
      long script.  This is the same as how the REPL operates.

      Supported code: source text.

Interpreter.set_main_attrs()의 기능 외에도, 이 모듈은 인터프리터 간에 데이터를 전달하는 관련 방법인 채널을 제공합니다. 아래의 Channels 절을 참조하십시오.

처리되지 않은 예외

Interpreter.exec()에서 처리되지 않은 예외에 관해, 우리는 그 예외가 interp.exec()가 호출된 코드로 “사실상” 전파된다고 언급했습니다. 인터프리터 간에 예외(및 트레이스백)가 누출되는 것을 막기 위해, 우리는 예외와 그 트레이스백의 대리 객체를 만들고(traceback.TracebackException 참조), 이를 새 interpreters.RunFailedError의 __cause__로 설정한 뒤 그 예외를 발생시킵니다.

예외(의 프록시)를 직접 발생시키면 interp.exec() 호출 자체에서 발생한 오류와 서브인터프리터에서 처리되지 않은 예외를 구별하기가 더 어려워지므로 문제가 됩니다.

인터프리터 제약 사항

interpreters.create()로 생성되는 모든 새 인터프리터는 이제 자신이 실행하는 모든 코드에 대해 특정 제약을 가집니다. 여기에는 다음이 포함됩니다.

  • 확장 모듈이 다단계 초기화(multi-phase init)를 구현하지 않으면 해당 모듈의 임포트가 실패합니다.
  • 데몬 스레드를 생성할 수 없습니다.
  • os.fork()는 허용되지 않습니다(따라서 multiprocessing도 사용할 수 없습니다).
  • os.exec*()는 허용되지 않습니다(하지만 subprocess 방식의 “fork+exec”는 괜찮습니다).

기존 C-API로 생성된 인터프리터에는 이러한 제약이 없다는 점에 유의하십시오. “메인” 인터프리터도 마찬가지이므로, 기존의 Python 사용 방식은 바뀌지 않습니다.

나중에 위의 제약 중 일부를 완화하거나, 세분화된 제약을 개별적으로 활성화/비활성화하는 방법을 제공할 수도 있습니다. 어느 쪽이든, 확장 모듈에 다단계 초기화를 요구하는 것은 항상 기본 제약으로 유지됩니다.

통신을 위한 API

앞의 Shared Data에서 논의했듯이, 여러 인터프리터 지원은 인터프리터 간에 데이터를 공유(통신)하는 메커니즘이 없으면 그 유용성이 떨어집니다. 그러나 인터프리터 간에 실제 Python 객체를 공유하는 것은 잠재적인 문제가 충분히 많으므로, 이 제안에서는 이를 지원하지 않습니다. 또한 앞서 언급했듯이, 기본적인 통신 메커니즘 이상의 것은 추가하지 않습니다.

그 메커니즘은 Interpreter.set_main_attrs() 메서드입니다. 이 메서드는 Interpreter.exec()가 호출되기 전에 전역 변수를 설정하는 데 사용할 수 있습니다. set_main_attrs()에 전달된 이름-값 쌍은 해당 인터프리터의 __main__ 모듈의 속성으로 바인딩됩니다. 값은 “공유 가능”해야 합니다. 아래의 Shareable Types를 참조하십시오.

Interpreter.set_main_attrs()를 통해 객체를 통신하고 공유하는 추가적인 방식이 가능해집니다. 큐처럼 동작하면서 인터프리터 간 안전성을 갖춘 공유 가능 객체를 구현할 수도 있습니다. 실제로 이 PEP에는 그러한 방식의 예인 채널이 포함되어 있습니다.

공유 가능 타입

객체의 타입이 공유 가능 인스턴스를 지원하면 그 객체는 “공유 가능”합니다. 해당 타입은 새로운 내부 프로토콜을 구현해야 하며, 이 프로토콜은 객체를 인터프리터에 독립적인 데이터로 변환한 다음 다른 쪽에서 다시 객체로 변환하는 데 사용됩니다. 위의 is_shareable()도 참조하십시오.

처음에는 다음을 포함하여 단순하고 불변인 최소한의 내장 타입 집합이 지원됩니다.

  • None
  • bool
  • bytes
  • str
  • int
  • float

처음에는 소수의 복합 타입도 지원할 것입니다:

  • memoryview, PEP 3118 버퍼를 공유할 수 있도록 합니다
  • 채널

복합 타입이든 아니든, 이후에 더 많은 내장 타입을 지원할 수 있습니다. 처음에 공유할 수 있는 타입을 제한하는 것은 실용적인 이유 때문이며, 초기 구현의 잠재적인 복잡성을 줄여 줍니다. 인터프리터 격리에 대한 경험이 더 쌓이면, 앞으로 지원하는 객체를 확장하기 위해 추진할 수 있는 여러 전략이 있습니다.

그동안 Interpreter.set_main_attrs()가 사용하는 내부 프로토콜(및 C-API)을 공개하는 방안은 별도의 제안에서 논의할 것입니다. 그 프로토콜을 사용하면 확장 모듈이 다른 타입에 대한 지원을 추가할 수 있습니다.

OS 파이프를 통한 통신

통신 전용 객체가 없더라도 사용자는 이미 기존 도구를 사용할 수 있습니다. 예를 들어, 인터프리터 간에 데이터를 보내는 기본적인 방법 중 하나는 파이프를 사용하는 것입니다(os.pipe() 참조):

  1. 인터프리터 A가 os.pipe()를 호출하여 읽기/쓰기 파일 디스크립터 쌍(둘 다 int 객체)을 얻습니다
  2. 인터프리터 A가 interp.set_main_attrs()를 호출하여 읽기 FD를 바인딩합니다(또는 문자열 포매팅을 사용하여 삽입합니다)
  3. 인터프리터 A가 인터프리터 B에 대해 interp.exec()를 호출합니다
  4. 인터프리터 A가 쓰기 FD에 몇 바이트를 씁니다
  5. 인터프리터 B가 그 바이트를 읽습니다

Synchronize using an OS pipe 등 앞선 여러 예제에서 이를 보여 줍니다.

채널

interpreters 모듈에는 인터프리터 간에 객체 데이터를 전달하기 위한 전용 수단인 채널이 포함될 것입니다. 채널이 모듈에 포함되는 이유는, 한편으로는 os.pipe()를 사용하는 것보다 쉬운 메커니즘을 제공하기 위해서이고, 다른 한편으로는 라이브러리가 Interpreter.set_main_attrs() 및 그것이 사용하는 프로토콜을 어떻게 활용할 수 있는지 보여 주기 위해서입니다.

채널은 단방향 FIFO입니다. 채널은 파이프, 큐, CSP의 채널에서 영감을 얻은 기본적인 옵트인(opt-in) 데이터 공유 메커니즘입니다. [fifo] 파이프와의 주요 차이점은 채널의 양쪽 끝에 각각 0개 이상의 인터프리터가 연결될 수 있다는 것입니다. 역시 다대다인 큐와 마찬가지로 채널은 버퍼링됩니다(다만 버퍼링되지 않는 의미 체계를 가진 메서드도 제공합니다).

채널에는 보내기와 받기라는 두 가지 연산이 있습니다. 이 연산들의 핵심 특징은 채널이 Python 객체 자체가 아니라 객체에서 추출한 데이터를 전송한다는 점입니다. 객체를 보낼 때 그 데이터가 추출됩니다. 다른 인터프리터에서 그 “객체”를 받을 때, 데이터는 해당 인터프리터가 소유하는 객체로 다시 변환됩니다.

이를 위해 변경 가능한 공유 상태는 어느 인터프리터도 아닌 Python 런타임이 관리합니다. 처음에는 공유 상태에 대해 interpreters.create_channel()이 제공하는 채널이라는 한 가지 타입의 객체만 지원할 것입니다. 그리고 채널은 인터프리터 간의 객체 전달을 신중하게 관리할 것입니다.

API를 최소한으로 유지하는 것을 포함한 이 접근 방식은 내부의 복잡성이 Python 사용자에게 더 이상 드러나지 않도록 하는 데 도움이 됩니다.

interpreters 모듈은 채널과 관련된 다음 함수를 제공합니다.:

create_channel() -> (RecvChannel, SendChannel):

   Create a new channel and return (recv, send), the RecvChannel
   and SendChannel corresponding to the ends of the channel.

   Both ends of the channel are supported "shared" objects (i.e.
   may be safely shared by different interpreters.  Thus they
   may be set using "Interpreter.set_main_attrs()".

이 모듈은 채널과 관련된 다음 클래스도 제공합니다.:

class RecvChannel(id):

   The receiving end of a channel.  An interpreter may use this to
   receive objects from another interpreter.  Any type supported by
   Interpreter.set_main_attrs() will be supported here, though at
   first only a few of the simple, immutable builtin types
   will be supported.

   id -> int:

      The channel's unique ID.  The "send" end has the same one.

   recv(*, timeout=None):

      Return the next object from the channel.  If none have been
      sent then wait until the next send (or until the timeout is hit).

      At the least, the object will be equivalent to the sent object.
      That will almost always mean the same type with the same data,
      though it could also be a compatible proxy.  Regardless, it may
      use a copy of that data or actually share the data.  That's up
      to the object's type.

   recv_nowait(default=None):

      Return the next object from the channel.  If none have been
      sent then return the default.  Otherwise, this is the same
      as the "recv()" method.


class SendChannel(id):

   The sending end of a channel.  An interpreter may use this to
   send objects to another interpreter.  Any type supported by
   Interpreter.set_main_attrs() will be supported here, though
   at first only a few of the simple, immutable builtin types
   will be supported.

   id -> int:

      The channel's unique ID.  The "recv" end has the same one.

   send(obj, *, timeout=None):

      Send the object (i.e. its data) to the "recv" end of the
      channel.  Wait until the object is received.  If the object
      is not shareable then ValueError is raised.

      The builtin memoryview is supported, so sending a buffer
      across involves first wrapping the object in a memoryview
      and then sending that.

   send_nowait(obj):

      Send the object to the "recv" end of the channel.  This
      behaves the same as "send()", except for the waiting part.
      If no interpreter is currently receiving (waiting on the
      other end) then queue the object and return False.  Otherwise
      return True.

공유 객체에 대한 주의 사항

다시 말하지만, Python 객체는 인터프리터 간에 공유되지 않습니다. 그러나 경우에 따라 그 객체가 감싸는 데이터는 단순히 복사되는 것이 아니라 실제로 공유됩니다. 한 가지 예로 PEP 3118 버퍼를 들 수 있습니다.

이러한 경우 원래 인터프리터의 객체는 다른 인터프리터에서 공유된 데이터가 더 이상 사용되지 않을 때까지 살아 있는 상태로 유지됩니다. 그런 다음 원래 인터프리터에서 이전에 공유되었던 데이터와 함께 객체가 평소처럼 소멸될 수 있습니다.

문서

interpreters 모듈에 대한 새 표준 라이브러리 문서 페이지에는 다음 내용이 포함됩니다:

  • (맨 위에) 확장 모듈이 여러 인터프리터를 반드시 지원할 필요는 없다는 명확한 안내
  • 서브인터프리터가 무엇인지에 대한 설명
  • 여러 인터프리터를 사용하는 방법(및 인터프리터 간 통신 방법)에 대한 간단한 예제
  • 여러 인터프리터 사용 시의 제약 사항 요약
  • (확장 모듈 유지 관리자를 위한) 여러 인터프리터 호환성을 보장하기 위한 자료 링크
  • 이 PEP에 있는 API 정보의 상당 부분

확장 모듈 유지 관리자를 위한 자료에 관한 문서는 이미 Isolating Extension Modules 하우투 페이지에 있습니다. 추가 도움말은 그곳에 추가됩니다. 예를 들어, 서브인터프리터와 호환되지 않는 자체 전역 상태를 유지하는 링크된 라이브러리를 다루는 전략을 논의하는 것이 도움이 될 수 있습니다.

새로운 interpreters 모듈이 확장 모듈 유지 관리자에게 미칠 수 있는 부정적인 영향을 완화하는 데 문서가 큰 역할을 할 것이라는 점에 유의하십시오.

또한 호환되지 않는 확장 모듈에 대한 ImportError를 갱신하여, 여러 인터프리터 호환성이 없기 때문에 발생한 오류이며 확장 모듈이 이를 반드시 제공할 필요는 없다는 점을 명확하게 알립니다. 이는 사용자의 기대치를 올바르게 설정하는 데 도움이 됩니다.

대안

새 모듈 대신 고려할 수 있는 한 가지 대안은 concurrent.futures에 인터프리터 지원을 추가하는 것입니다. 이 방식이 통하지 않는 데에는 몇 가지 이유가 있습니다:

  • “threading” 등과 마찬가지로, 여러 인터프리터 지원을 찾을 때 가장 먼저 떠올릴 곳은 “interpreters” 모듈입니다.
  • concurrent.futures는 전적으로 함수 실행에 관한 것이지만, 현재로서는 한 인터프리터의 함수를 다른 인터프리터에서 실행할 좋은 방법이 없습니다.

multiprocessing 모듈에서의 지원에도 비슷한 논리가 적용됩니다.

미해결 질문

  • interp.exec()가 현재 스레드에서 실행된다는 점이 너무 혼란스럽지는 않겠습니까?
  • interp.exec(), 그리고/또는 Interpreter.set_main_attrs()와 Interpreter.get_main_attr()에 대한 피클링 대체 수단을 지금 바로 추가해야 합니까?
  • interp.exec()에서 (제한된) 함수를 지금 바로 지원해야 합니까?
  • Interpreter.close()의 이름을 Interpreter.destroy()로 바꿔야 합니까?
  • 채널이 있으므로 Interpreter.get_main_attr()를 제거해야 합니까?
  • 채널은 별도의 PEP로 다루어야 합니까?

보류된 기능

이 제안을 최소한으로 유지하기 위해, 다음 기능은 향후 검토 대상으로 남겨 두었습니다. 이는 해당 기능 중 어느 것에 대해서도 부정적인 판단을 내린 것이 아니라 보류한 것일 뿐이라는 점에 유의하십시오. 그렇기는 하지만, 각 기능은 충분히 타당하다고 볼 수 있습니다.

편의 API 추가

새 모듈의 가상의 불편한 부분을 매끄럽게 해 줄 수 있으리라 생각되는 것이 여러 가지 있습니다:

  • interp.exec()를 호출하고 pickle로 폴백하는 Interpreter.run() 또는 Interpreter.call() 같은 것을 추가
  • Interpreter.set_main_attrs()와 Interpreter.get_main_attr()에서 pickle로 폴백

이것이 불편한 점으로 드러난다면 이러한 작업은 쉽게 할 수 있습니다.

현재 스레드에서 실행되는 인터프리터에 대한 혼란 방지

자주 혼동되는 점 중 하나는 Interpreter.exec()가 현재 OS 스레드에서 실행되어 현재 Python 스레드를 일시적으로 블로킹한다는 것입니다. 그러한 혼란을 피하기 위해 무언가 조치를 취할 가치가 있을 수 있습니다.

이 가상의 문제에 대해 가능한 해결책은 다음과 같습니다:

  • 기본적으로 새 스레드에서 실행해야 합니까?
  • Interpreter.exec_in_thread()를 추가해야 합니까?
  • Interpreter.exec_in_current_thread()를 추가해야 합니까?

이 PEP의 이전 버전에서는 이 메서드가 interp.run()이었습니다. 문서를 통한 사용자 교육을 함께 한다면, interp.exec()로 간단히 바꾸는 것만으로도 혼란을 충분히 줄일 수 있을 것입니다. 이것이 실제 문제로 드러난다면, 그때 대안 중 하나를 추진할 수 있습니다.

“실행 중”과 “스레드를 가짐”의 구분 명확화

Interpreter.is_running()은 구체적으로 Interpreter.exec() (또는 이와 유사한 것)가 어딘가에서 실행 중인지 여부를 나타냅니다. 인터프리터에 실행 중인 서브스레드가 있는지에 대해서는 아무것도 알려 주지 않습니다. 그러한 정보가 유용할 수 있습니다.

할 수 있는 일은 다음과 같습니다:

  • Interpreter.is_running()의 이름을 Interpreter.is_running_main()으로 변경
  • Interpreter.is_running()을 보완하기 위해 Interpreter.has_threads() 추가
  • Interpreter.is_running(main=True, threads=False)로 확장

이 중 어느 것도 시급하지 않으며, 원한다면 어느 것이든 나중에 할 수 있습니다.

공유를 위한 던더 메서드

tp_xid에 대응하는 __xid__ 같은 특수 메서드를 추가할 수 있습니다. 적어도 Python 타입이 자신의 인스턴스를 tp_xid를 구현하는 다른 타입으로 변환할 수 있게 됩니다.

문제는 이 기능을 Python 코드에 노출하면 아직 검토되지 않은 수준의 복잡성이 생기며, 그 복잡성을 조사해야 할 설득력 있는 이유도 없다는 것입니다.

Interpreter.call()

기존 함수를 서브인터프리터에서 직접 실행할 수 있다면 편리할 것입니다. Interpreter.exec()를 조정하여 이를 지원하거나 call() 메서드를 추가할 수 있습니다.:

Interpreter.call(f, *args, **kwargs)

이 방식은 큐를 통해 인터프리터 간에 객체를 공유하는 것과 같은 문제를 겪습니다. 최소한의 해결책(소스 문자열 실행)만으로도 이 기능을 공개하여 탐구할 수 있게 하기에 충분합니다.

Interpreter.run_in_thread()

이 메서드는 스레드에서 interp.exec() 호출을 대신 수행해 줍니다. threading.Thread와 interp.exec()만으로 이를 구현하는 것은 비교적 간단하므로 포함하지 않았습니다.

동기화 프리미티브

threading 모듈은 동시 작업을 조정하기 위한 여러 동기화 프리미티브를 제공합니다. 이는 스레딩이 상태를 공유한다는 특성 때문에 특히 필요합니다. 반면에 인터프리터는 상태를 공유하지 않습니다. 데이터 공유는 런타임의 공유 가능 객체 기능으로 제한되며, 이 덕분에 명시적인 동기화가 필요하지 않습니다. 향후 CPython의 인터프리터에 선택적(opt-in) 상태 공유 지원이 어떤 형태로든 추가된다면, 그 작업에서 해당 필요를 충족하기 위한 동기화 프리미티브를 함께 도입할 수 있습니다.

CSP 라이브러리

csp 모듈은 이 PEP가 제공하는 기능에서 크게 멀리 떨어져 있지 않습니다. 하지만 그러한 모듈을 추가하는 것은 이 제안의 최소주의적 목표를 벗어납니다.

구문 지원

Go 언어는 CSP에 기반한 동시성 모델을 제공하므로, 여러 인터프리터가 지원하는 동시성 모델과 유사합니다. 하지만 Go는 동시성을 일급 기능으로 만들기 위해 구문 지원과 여러 내장 동시성 프리미티브도 제공합니다. 인터프리터를 사용하여 Python에도 유사한 문법적(및 내장) 지원을 추가하는 것을 생각해 볼 수 있습니다. 하지만 그것은 이 PEP의 범위를 한참 벗어납니다!

멀티프로세싱

multiprocessing 모듈은 스레드와 프로세스를 지원하는 것과 같은 방식으로 인터프리터를 지원할 수 있습니다. 실제로 이 모듈의 유지 관리자인 Davin Potts는 이것이 합리적인 기능 요청이라고 밝혔습니다. 하지만 이는 이 PEP의 좁은 범위를 벗어납니다.

C 확장 옵트인/옵트아웃

PEP 489에서 도입된 PyModuleDef_Slot을 사용하면, C 확장 모듈이 여러 인터프리터 지원에서 빠질 수 있는 메커니즘을 쉽게 추가할 수 있습니다. 그러면 임포트 메커니즘은 서브인터프리터에서 동작할 때 해당 모듈이 이를 지원하는지 확인해야 합니다. 지원하지 않는 경우 ImportError를 발생시킵니다.

또는 여러 인터프리터 지원에 옵트인하는 방식을 지원할 수도 있습니다. 그러나 그렇게 하면 옵트아웃(opt-out) 방식보다 (불필요하게) 훨씬 더 많은 모듈이 제외될 가능성이 높습니다. 또한 PEP 489에서 확장이 해당 PEP의 메커니즘을 사용하면 여러 인터프리터 지원을 의미한다고 정의했다는 점에 유의하십시오.

ModuleDef 슬롯을 추가하고 임포트 메커니즘을 수정하는 작업의 범위는 결코 작지 않지만, 그만한 가치가 있을 수 있습니다. 이는 전적으로 서브인터프리터에서 동작하지 않는 확장 모듈이 얼마나 많은지에 달려 있습니다. mod_wsgi를 통해 알려진 사례가 비교적 적다는 점을 고려하면, 이 작업은 나중으로 미룰 수 있습니다.

채널 오염시키기

CSP에는 채널을 오염(poison)시키는 개념이 있습니다. 채널이 한번 오염되면 그 채널에 대한 모든 send() 또는 recv() 호출은 특별한 예외를 발생시키며, 오염된 채널을 사용하려 한 인터프리터의 실행을 사실상 종료시킵니다.

이는 채널의 양쪽 끝에 poison() 메서드를 추가하여 구현할 수 있습니다. close() 메서드를 (대체로) 이런 방식으로 사용할 수 있지만, 이러한 의미 체계는 비교적 특수하므로 나중으로 미룰 수 있습니다.

__main__ 초기화하기

제안된 대로라면 Interpreter.exec()를 호출할 때마다 인터프리터에 이미 존재하는 __main__ 모듈의 네임스페이스에서 실행됩니다. 즉, interp.exec() 호출 사이에 데이터가 그곳에 유지됩니다. 때로는 이것이 바람직하지 않아서 새로운 __main__에서 실행하고 싶을 수 있습니다. 또한 더 이상 사용하지 않는 객체를 그곳에 남겨 두고 싶지 않을 수도 있습니다.

다음 방법은 너무 많은 것(예: __name__ 및 기타 “__dunder__” 속성)을 지워 버리므로 제대로 동작하지 않는다는 점에 유의하십시오.:

interp.exec('globals().clear()')

가능한 해결책은 다음과 같습니다.

  • 각 interp.exec() 호출 후에 __main__을 초기화하도록 지정하는 create() 인자
  • 나중에 옵트인하거나 옵트아웃할 수 있도록 지원하는 Interpreter.reset_main 플래그
  • 원할 때 옵트인할 수 있는 Interpreter.reset_main() 메서드
  • importlib.util.reset_globals() [reset_globals]

또한 __main__을 초기화해도 다른 모듈에 저장된 상태에는 아무런 영향이 없다는 점에 유의하십시오. 따라서 어떤 해결책이든 무엇을 초기화하는지 그 범위를 명확히 해야 합니다. 모듈에 로드하기 전에 모듈을 비우지 않는 reload()와 달리, 임의의(또는 모든) 모듈을 초기화할 수 있는 메커니즘을 고안할 수도 있을 것입니다.

어쨌든 __main__은 인터프리터의 실행 네임스페이스이므로, 이를 초기화하는 것은 다른 모듈을 초기화하는 것보다 인터프리터 및 그 동적 상태와 훨씬 더 직접적인 관련이 있습니다. 따라서 더 범용적인 모듈 초기화 메커니즘은 불필요한 것으로 판명될 수도 있습니다.

이 기능은 초기에는 중요하지 않습니다. 필요하다면 나중으로 미룰 수 있습니다.

인터프리터의 상태 초기화하기

새 서브인터프리터를 띄우는 대신 기존 서브인터프리터를 재사용할 수 있다면 좋을 것입니다. 인터프리터에는 __main__ 모듈 외에도 훨씬 많은 상태가 있으므로, 인터프리터를 처음 생성된 깨끗한 상태로 되돌리기는 그리 쉽지 않습니다. 사실, 상태 중에는 Python 코드에서 재설정할 수 없는 부분이 있을 수도 있습니다.

한 가지 가능한 해결책은 Interpreter.reset() 메서드를 추가하는 것입니다. 이 메서드는 인터프리터를 새로 생성되었을 때의 상태로 되돌립니다. 실행 중인 인터프리터에서 호출하면 실패합니다(따라서 메인 인터프리터는 결코 재설정할 수 없습니다). 이 방법은 새 인터프리터를 생성하는 것보다 효율적일 가능성이 높지만, 이는 앞으로 인터프리터 생성에 어떤 최적화가 이루어지는지에 따라 달라집니다.

이 기능은 Python 코드에서 달리 사용할 수 없는 기능을 제공할 수도 있지만, 근본적인 기능은 아닙니다. 따라서 여기서는 최소주의 정신에 따라 이 기능을 나중으로 미룰 수 있습니다. 어쨌든 PEP 이후에 이 기능을 추가하는 것이 논란이 되지는 않을 것이라고 생각합니다.

기존 인터프리터의 상태 복사

이와 관련하여, 기존 인터프리터를 기반으로 새 인터프리터를 생성하는 기능(예: Interpreter.copy())을 지원하는 것이 유용할 수 있습니다. 이는 인터프리터 메모리의 스냅숏을 만들 수 있다는 아이디어와 연결되며, 이를 통해 CPython 시작이나 새 인터프리터 생성이 전반적으로 더 빨라질 것입니다. 앞서 설명한 가상의 Interpreter.reset()에도 같은 메커니즘을 사용할 수 있습니다.

공유 가능한 파일 디스크립터와 소켓

파일 디스크립터와 소켓은 프로세스 전역 자원이므로, 이를 공유 가능하게 만드는 것은 합리적인 아이디어입니다. 이들은 지원되는 공유 가능 타입을 확장하려는 첫 번째 시도의 좋은 후보가 될 것입니다. 초기 API에 반드시 필요한 것은 아닙니다.

async와의 통합

Antoine Pitrou에 따르면 [async]:

Has any thought been given to how FIFOs could integrate with async
code driven by an event loop (e.g. asyncio)?  I think the model of
executing several asyncio (or Tornado) applications each in their
own subinterpreter may prove quite interesting to reconcile multi-
core concurrency with ease of programming.  That would require the
FIFOs to be able to synchronize on something an event loop can wait
on (probably a file descriptor?).

여러 인터프리터 지원의 기본 기능은 async에 의존하지 않으며 나중에 추가할 수 있습니다.

한 가지 가능한 해결책은 블로킹 채널 메서드(recv(), send())의 async 구현을 제공하는 것입니다.

또는 async 시나리오에서의 사용을 단순화하기 위해 “준비 완료 콜백(readiness callbacks)”을 사용할 수 있습니다. 이는 recv_nowait() 및 send_nowait() 채널 메서드에 선택적인 callback (키워드 전용) 매개변수를 추가한다는 뜻입니다. 콜백은 객체가 (각각) 전송되거나 수신되면 호출됩니다.

(채널을 버퍼링하면 준비 완료 콜백의 중요성이 줄어든다는 점에 유의하십시오.)

이터레이션 지원

(__iter__() 또는 _next__()를 통해) RecvChannel에 대한 이터레이션을 지원하는 것이 유용할 수 있습니다. 간단한 구현은 파일이 이터레이션을 수행하는 방식과 유사하게 recv() 메서드를 사용할 것입니다. 이는 근본적인 기능이 아니고 간단한 대안이 있으므로, 이터레이션 지원 추가는 나중으로 미룰 수 있습니다.

채널 컨텍스트 관리자

RecvChannel과 SendChannel에서 컨텍스트 관리자를 지원하면 유용할 수 있습니다. 파일이 그러하듯이 close() (또는 release()) 호출을 감싸기만 하면 되므로 구현은 간단할 것입니다. 이터레이션과 마찬가지로 이 기능은 나중으로 미뤄도 됩니다.

파이프와 큐

“os.pipe()”라는 제안된 객체 전달 메커니즘이 있으므로, 여러 인터프리터의 최소한의 유용한 기능을 달성하는 데 다른 유사한 기본 타입이 반드시 필요하지는 않습니다. 그러한 타입으로는 파이프(버퍼 없는 채널과 비슷하지만 일대일)와 큐(채널과 비슷하지만 더 범용적)가 있습니다. 자세한 내용은 아래의 Rejected Ideas를 참조하십시오.

이러한 타입은 이 제안에 포함되지 않지만, 동시성의 맥락에서는 여전히 유용할 수 있습니다. 나중에 추가하는 것도 충분히 합리적입니다. 이들은 채널을 감싸는 래퍼로 손쉽게 구현할 수 있습니다. 또는 효율성을 위해 채널과 동일한 저수준에서 구현할 수도 있습니다.

send()에서 잠금 반환하기

채널을 통해 객체를 보낼 때, 반대편에서 객체가 언제 수신되는지 알 방법이 없습니다. 이를 우회하는 한 가지 방법은 SendChannel.send()에서 잠긴 threading.Lock을 반환하고, 객체가 수신되면 잠금이 해제되도록 하는 것입니다.

그 대신, 제안된 SendChannel.send() (블로킹)와 SendChannel.send_nowait()는 사용자를 혼란스럽게 할 가능성이 더 낮은 명시적인 구분을 제공합니다.

잠금을 반환하는 것은 버퍼가 있는 채널(즉, 큐)에서 의미가 있다는 점에 유의하십시오. 버퍼 없는 채널에서는 문제가 되지 않습니다.

채널에서 우선순위 지정 지원

간단한 예로 표준 라이브러리의 queue.PriorityQueue가 있습니다.

설정 상속 지원 (그리고 그 이상?)

새 인터프리터를 생성할 때 일부 항목을 새 인터프리터가 “상속”하도록 지정할 수 있으면 유용하다고 생각하는 사람들이 있을 수 있습니다. 이 메커니즘은 엄격한 복사일 수도 있고 쓰기 시 복사(copy-on-write)일 수도 있습니다. 이 아이디어의 계기가 된 예는 warnings 모듈입니다(예: 필터 복사).

이 기능은 중요하지도 않고 널리 유용하지도 않으므로, 관심이 생길 때까지 미룰 수 있습니다. 특히 제안된 두 해결책 모두 상당한 작업이 필요하며, 복잡한 객체의 경우에 더욱 그렇고, 가변 복합 객체를 담은 가변 컨테이너의 경우에는 무엇보다도 그렇습니다.

예외를 공유 가능하게 만들기

예외는 run() 호출 밖으로 전파되므로, 예외를 공유 가능하게 만드는 것은 그리 큰 비약이 아닙니다. 그러나 다른 곳에서 언급했듯이, 이는 필수적이지도 (특별히 흔하지도) 않으므로 해당 작업은 나중으로 미룰 수 있습니다.

직렬화를 통해 모든 것을 공유 가능하게 만들기

pickle(또는 marshal)을 사용해 모든 것을 직렬화하여 공유 가능하게 만들 수 있습니다. 이렇게 하면 비효율적일 수 있지만, 결국에는 편의성의 문제일 수 있습니다. 나중에 추가할 수는 있지만, 나중에 제거하려고 하면 훨씬 더 고통스러울 것입니다.

RunFailedError.__cause__를 지연 처리하기

서브인터프리터에서 (interp.exec()로부터) 처리되지 않은 예외는 호출한 인터프리터로 복사되어 RunFailedError의 __cause__로 설정되며, 그 후 이 RunFailedError가 발생합니다. 이 복사 과정에는 호출한 인터프리터에서의 일종의 역직렬화가 포함되는데, 이는 (예를 들어 임포트 때문에) 비용이 클 수 있지만 항상 필요한 것은 아닙니다.

따라서 ExceptionProxy 타입을 사용하여 직렬화된 예외를 감싸고, 필요할 때만 역직렬화하는 것이 유용할 수 있습니다. 이는 ExceptionProxy__getattribute__()를 통하거나, 어쩌면 RunFailedError.resolve()를 통해 이루어질 수 있습니다(이 메서드는 역직렬화된 예외를 발생시키고 RunFailedError.__cause__를 그 예외로 설정합니다.

RunFailedError.__cause__를 RunFailedError 인스턴스에서 지연 역직렬화를 수행하는(그리고 __cause__를 설정하는) 디스크립터로 만드는 것도 타당할 수 있습니다.

interp.exec()에서 값 반환하기

현재 interp.exec()는 항상 None을 반환합니다. 한 가지 아이디어는 서브인터프리터가 실행한 것의 반환값을 반환하는 것입니다. 그러나 지금으로서는 이는 타당하지 않습니다. 사용자가 실행할 수 있는 것은 코드 문자열(즉, 스크립트)뿐입니다. 이는 PyRun_StringFlags(), exec(), 또는 모듈 본문과 동등합니다. 그중 어느 것도 아무것도 “반환”하지 않습니다. interp.exec()가 함수 등을 지원하게 되면 이 문제를 다시 검토할 수 있습니다.

공유 가능한 동기화 프리미티브 추가하기

이는 인터프리터들이 실제로 하부 뮤텍스를 공유하는 _threading.Lock(또는 그와 유사한 것)이 될 것입니다. 주된 우려는 (Go에서 배웠듯이) 잠금과 격리된 인터프리터가 잘 어울리지 않을 수 있다는 점입니다.

바람직하다고 판명되면 나중에 큰 어려움 없이 추가할 수 있습니다.

SystemExit와 KeyboardInterrupt를 다르게 전파하기

(Exception을 제외하고) BaseException을 상속하는 예외 타입들은 보통 특별하게 취급됩니다. 이러한 타입은 KeyboardInterrupt, SystemExit, GeneratorExit입니다. interp.exec()로부터의 전파에 관해서는 이들을 특별하게 취급하는 것이 타당할 수 있습니다. 다음은 몇 가지 선택지입니다:

* propagate like normal via RunFailedError
* do not propagate (handle them somehow in the subinterpreter)
* propagate them directly (avoid RunFailedError)
* propagate them directly (set RunFailedError as __cause__)

이들을 다르게 처리하는 문제는 신경 쓰지 않겠습니다. 스레드는 이미 SystemExit를 무시하므로, 당분간은 그 방식을 따르겠습니다.

채널 끝 클래스에 명시적인 release()와 close() 추가하기

이후 전역적으로 사용하지 못하도록 채널을 명시적으로 닫는 방법이 있으면 편리할 수 있습니다. 마찬가지로 현재 인터프리터를 기준으로 채널 끝 중 하나를 명시적으로 해제하는 방법이 있으면 유용할 수 있습니다. 여러 이유 중에서도, 이러한 메커니즘은 채널을 통해 객체를 직접 전달할 때 필요한 추가적인 상용구 코드 없이 인터프리터 간에 전체 상태를 전달하는 데 유용합니다.

어려운 점은 이해하기 어렵게 만들지 않으면서 자동 해제/닫기를 올바르게 구현하는 것입니다. 이는 비어 있지 않은 채널을 다룰 때 특히 그렇습니다. 당분간은 release/close 없이도 충분히 해 나갈 수 있을 것입니다.

SendChannel.send_buffer() 추가

이 메서드를 사용하면 객체가 PEP 3118 버퍼 프로토콜(예: memoryview)을 지원하는 경우 해당 객체를 복사 없이 채널로 보낼 수 있습니다.

이 기능은 채널의 핵심 기능이 아니므로 나중에 큰 혼란 없이 추가할 수 있습니다.

스레드에서 자동 실행

이 PEP는 서브인터프리터와 스레드를 엄격하게 분리할 것을 제안합니다. 스레드에서 실행하려면 스레드를 직접 생성하고 그 안에서 interp.exec()를 호출해야 합니다. 하지만 interp.exec()가 이를 대신 해 준다면 편리할 수 있으며, 그만큼 상용구 코드도 줄어들 것입니다.

또한 사용자는 대부분의 경우 스레드에서 실행하기를 원할 것으로 예상합니다. 따라서 이를 기본 동작으로 만드는 것이 합리적입니다. 현재 스레드에서 실행하는 동작도 할 수 있도록 interp.exec()에 키워드 전용 매개변수 “threaded”(기본값 True)를 추가할 것입니다.

기각된 아이디어

명시적 채널 연결

인터프리터는 recv() 및 send()를 호출할 때 채널과 암묵적으로 연결됩니다. 연결은 release()를 호출할 때 해제됩니다. 대안은 명시적인 메서드를 두는 것입니다. 이는 Interpreter 객체의 add_channel() 및 remove_channel() 메서드이거나, 채널 객체에 있는 이와 비슷한 메서드일 것입니다.

실제로 사용자에게는 이 정도 수준의 관리가 필요하지 않을 것입니다. 따라서 명시적인 지원을 더 추가하면 API만 복잡해질 뿐입니다.

파이프 기반 API 추가

파이프는 정확히 두 인터프리터 사이의 단방향 FIFO가 될 것입니다. 대부분의 사용 사례에서는 이것으로 충분할 것입니다. 구현도 더 단순해질 가능성이 있습니다. 하지만 여기서 한 걸음만 더 나아가면 채널을 통해 다대다 단방향 FIFO를 지원할 수 있습니다. 또한 파이프를 사용하면 파이프에 이름을 붙여야 하므로 API가 결국 약간 더 복잡해집니다.

큐 기반 API 추가

큐와 버퍼가 있는 채널은 거의 같은 것입니다. 주요 차이점은 채널이 컨텍스트(즉, 연결된 인터프리터)와 더 밀접한 관계를 가진다는 점입니다.

표준 라이브러리의 queue.Queue와 혼동하지 않도록 “Queue”(큐) 대신 “Channel”이라는 이름을 사용했습니다.

“enumerate”

list_all() 함수는 모든 인터프리터의 목록을 제공합니다. 제안된 API에 부분적으로 영향을 준 threading 모듈에서는 이 함수를 enumerate()라고 부릅니다. 여기서 이름을 다르게 한 것은 threading API에 익숙하지 않은 Python 사용자가 혼란스러워하지 않도록 하기 위해서입니다. 그런 사용자에게 “enumerate”는 다소 불분명하지만, “list_all”은 명확합니다.

인터프리터 간 예외 누출을 막기 위한 대안

함수 호출에서는 잡히지 않은 예외가 호출한 프레임으로 전파됩니다. interp.exec()에도 같은 방식을 적용할 수 있습니다. 하지만 그렇게 하면 예외 객체가 인터프리터 간 경계를 넘어 누출됩니다. 마찬가지로 트레이스백의 프레임도 누출될 수 있습니다.

지금은 문제가 되지 않을 수도 있지만, 인터프리터가 메모리 관리 측면에서 더 잘 격리되면(이는 인터프리터 간 GIL 공유를 중단하는 데 필요합니다) 문제가 될 것입니다. 그 대신 RunFailedError를 발생시키고, 그 __cause__가 원래 예외와 트레이스백에 대한 안전한 프록시를 감싸도록 하여 예외가 전파되는 방식의 의미 체계를 정리했습니다.

기각된 해결책 후보:

  • 원래 인터프리터에서 예외와 트레이스백을 재현하여 발생시킵니다.
  • 원래 예외와 트레이스백을 프록시하는 RunFailedError의 서브클래스를 발생시킵니다.
  • RunFailedError 대신 RuntimeError를 발생시킵니다.
  • 경계에서 변환합니다(subprocess.CalledProcessError처럼) (인터프리터 간 표현이 필요합니다).
  • Interpreter.excepthook을 통한 사용자 정의를 지원합니다(인터프리터 간 표현이 필요합니다).
  • 경계에서 프록시로 감쌉니다(트레이스백을 전파하는 err.raise() 같은 기능의 지원도 포함합니다).
  • 예외를 발생시키는 대신 interp.exec()에서 예외(또는 그 프록시)를 반환합니다.
  • (subprocess처럼) 결과 객체를 반환합니다 [result-object] (불필요하게 복잡하지 않습니까?)
  • 예외를 버리고, 사용자가 interp.exec()에 전달하는 스크립트 안에서 처리되지 않은 예외를 명시적으로 다루도록 합니다(채널을 통해 오류 정보를 밖으로 전달할 수 있습니다). 스레드를 사용할 때도 비슷하게 해야 합니다.

새 인터프리터마다 항상 전용 스레드를 연결하는 방안

C-API에 구현된 대로라면 인터프리터는 본질적으로 어떤 스레드에도 묶여 있지 않습니다. 게다가 인터프리터는 Python이 생성했는지와 관계없이 기존의 어떤 스레드에서든 실행됩니다. 먼저 해당 스레드에서 그 인터프리터의 스레드 상태(PyThreadState) 중 하나를 활성화하기만 하면 됩니다. 즉, 한 스레드에서 둘 이상의 인터프리터를 실행할 수 있습니다(물론 동시에 실행할 수는 없습니다).

제안하는 모듈도 이 동작을 유지합니다. 인터프리터는 스레드에 묶이지 않습니다. Interpreter.exec() 호출만 스레드에 묶입니다. 하지만 이 PEP의 핵심 목표 중 하나는 더 사람 중심적인 동시성 모델을 제공하는 것입니다. 이 점을 고려하면, 각 인터프리터에 전용 스레드가 연결되어 있을 때 개념적으로는 이 모듈을 이해하기가 더 쉬울 수도 있습니다.

이는 interpreters.create()가 새 스레드를 생성하고, Interpreter.exec()는 그 스레드에서만 실행된다는(그리고 그 스레드에서는 다른 어떤 것도 실행되지 않는다는) 뜻입니다. 이 방식의 이점은 사용자가 Interpreter.exec() 호출을 새 threading.Thread로 감쌀 필요가 없다는 것입니다. 또한 자신의 인터프리터가 실행되는 동안 (현재 스레드의) 현재 인터프리터를 실수로 멈추게 되는 일도 없을 것입니다.

이 아이디어는 이점은 작고 비용은 크기 때문에 기각되었습니다. C-API의 기능과 다르다는 점이 혼란을 줄 수 있습니다. 스레드를 암묵적으로 생성하는 것은 마법 같은 동작입니다. 스레드를 미리 생성하는 것은 낭비가 될 수 있습니다. 기존 스레드에서 임의의 인터프리터를 실행할 수 없게 되면 일부 타당한 사용 사례가 막혀 사용자를 좌절시킬 것입니다. 인터프리터를 스레드에 묶으려면 런타임을 추가로 수정해야 합니다. 또한 모듈의 구현이 지나치게 복잡해질 것입니다. 마지막으로, 그렇게 한다고 해서 모듈을 이해하기가 더 쉬워지지 않을 수도 있습니다.

사용할 때에만 인터프리터 연결하기

recv(), send() 등이 호출될 때에만 인터프리터를 채널 끝과 연결합니다.

이렇게 하면 혼란을 줄 수 있으며, 원래의 (생성한) 인터프리터에서 채널을 사용하기도 전에 채널이 자동으로 닫히는 예기치 않은 경쟁 상태가 발생할 수도 있습니다.

Interpreter.exec()의 동시 다중 호출 허용하기

이는 특히 Interpreter.exec()가 새 스레드를 대신 관리해 준다면(이 방안은 기각되었습니다) 합리적일 것입니다. 본질적으로 각 호출은 독립적으로 실행될 것이며, 각 인터프리터는 여러 스레드를 가질 수 있으므로 좁은 기술적 관점에서는 대체로 문제가 없을 것입니다.

문제는 인터프리터에 __main__ 모듈이 하나뿐이어서, 동시에 이루어지는 Interpreter.exec() 호출들이 __main__ 공유 문제를 해결해야 하거나 새로운 메커니즘을 고안해야 한다는 점입니다. 어느 쪽도 할 가치가 있을 만큼 간단하지 않습니다.

RunFailedError에 “reraise” 메서드 추가하기

RunFailedError에 __cause__를 설정하면 더 유용한 트레이스백을 만드는 데 도움이 되지만, 원래의 오류를 처리할 때는 그다지 도움이 되지 않습니다. 이를 용이하게 하기 위해 RunFailedError.reraise()를 추가할 수 있습니다. 이 메서드를 사용하면 다음과 같은 패턴이 가능해집니다:

try:
    try:
        interp.exec(script)
    except RunFailedError as exc:
        exc.reraise()
except MyException:
    ...

__reraise__ 프로토콜이 있다면 이는 훨씬 더 간단해질 것입니다.

그렇긴 하지만, 이는 전혀 필요하지 않습니다. __cause__를 사용하는 것으로 충분합니다:

try:
    try:
        interp.exec(script)
    except RunFailedError as exc:
        raise exc.__cause__
except MyException:
    ...

극단적인 경우에는 약간의 보일러플레이트가 추가로 필요할 수 있다는 점에 유의하십시오:

try:
    try:
        interp.exec(script)
    except RunFailedError as exc:
        if exc.__cause__ is not None:
            raise exc.__cause__
        raise  # re-raise
except MyException:
    ...

구현

이 PEP의 구현은 4개 부분으로 이루어져 있습니다:

  • 이 PEP에서 설명하는 고수준 모듈(대부분 저수준 C 확장을 감싸는 가벼운 래퍼
  • 저수준 C 확장 모듈
  • 저수준 모듈에 필요한 내부 C-API 추가 사항
  • 저수준 모듈을 용이하게 하는 (다른 이점도 있는) CPython 런타임의 부차적인 수정/변경 사항

이들은 완성 단계가 제각각이며, 아래로 갈수록 더 많이 완료되어 있습니다:

  • 고수준 모듈은 기껏해야 대략적으로만 구현되어 있습니다. 하지만 이를 완전히 구현하는 일은 거의 사소할 것입니다.
  • 저수준 모듈은 대부분 완성되었습니다. 구현의 대부분은 2018년 12월에 “_xxsubinterpreters” 모듈로 master에 병합되었습니다(여러 인터프리터 기능을 테스트하기 위해서입니다). 예외 전파 구현만 마무리하면 되며, 이는 많은 작업을 필요로 하지 않을 것입니다.
  • 필요한 모든 C-API 작업이 완료되었습니다
  • 런타임에서 예상되었던 모든 작업이 완료되었습니다

PEP 554의 구현 작업은 CPython의 멀티코어 지원 개선을 목표로 하는 더 큰 프로젝트의 일부로 추적되고 있습니다. [multi-core-project]

참고 문헌