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

Python 개선 제안 한국어 번역

PEP 669 – CPython을 위한 저영향 모니터링

Author:
Mark Shannon <mark at hotpy.org>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Created:
18-Aug-2021
Python-Version:
3.12
Post-History:
07-Dec-2021, 10-Jan-2022
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at sys.monitoring.

×

See PEP 1 for how to propose changes.

초록

CPython에서 프로파일러나 디버거를 사용하면 성능에 심각한 영향을 줄 수 있습니다. 10배 수준의 속도 저하도 흔히 발생합니다.

이 PEP는 CPython에서 실행되는 Python 프로그램을 적은 비용으로 모니터링할 수 있게 하는 API를 제안합니다.

이 PEP는 구현을 규정하지 않지만, PEP 659의 퀴크닝(quickening) 단계를 사용하여 구현될 것으로 예상합니다.

sys.monitoring 네임스페이스가 추가되며, 여기에 관련 함수와 상수가 포함됩니다.

동기

개발자가 디버거, 프로파일러 및 기타 유사한 도구를 사용하기 위해 불합리한 비용을 치러야 해서는 안 됩니다.

C++와 Java 개발자는 디버거 아래에서도 프로그램을 최고 속도로(또는 그에 매우 가깝게) 실행할 수 있기를 기대합니다. Python 개발자도 그렇게 기대할 수 있어야 합니다.

근거

PEP 659가 제공하는 퀴크닝 메커니즘은 실행 중인 Python 바이트코드를 동적으로 수정하는 방법을 제공합니다. 이러한 수정은 수정되는 코드 부분 외에는 비용이 거의 들지 않으며, 수정되는 부분에도 비교적 적은 비용만 듭니다. 이를 활용하면 3.10 이하 버전에서는 불가능했던 효율적인 모니터링 메커니즘을 제공할 수 있습니다.

퀴크닝을 사용하면 3.12에서 디버거 아래에서 실행되는 코드가 3.11에서 디버거 없이 실행되는 코드보다 더 빠를 것으로 예상합니다. 프로파일링은 여전히 실행 속도를 떨어뜨리지만, 그 정도는 3.11보다 훨씬 적습니다.

명세

Python 프로그램의 모니터링은 이벤트에 대한 콜백 함수를 등록하고 이벤트 집합을 활성화하는 방식으로 이루어집니다.

이벤트 활성화와 콜백 함수 등록은 서로 독립적입니다.

콜백 등록과 이벤트 활성화는 모두 도구별로 이루어집니다. 서로 다른 이벤트 집합에 응답하는 여러 도구를 둘 수 있습니다.

sys.settrace()와 달리 이벤트와 콜백은 스레드별이 아니라 인터프리터별이라는 점에 유의하십시오.

이벤트

코드 객체가 실행되는 동안 도구가 관심을 가질 만한 다양한 이벤트가 발생합니다. 도구는 이벤트를 활성화하고 콜백 함수를 등록함으로써 이러한 이벤트에 자신에게 맞는 방식으로 응답할 수 있습니다. 이벤트는 전역으로 설정하거나 개별 코드 객체에 대해 설정할 수 있습니다.

3.12에서 CPython은 다음 이벤트를 지원합니다:

  • PY_START: Python 함수의 시작(호출 직후에 발생하며, 피호출자의 프레임이 스택에 있습니다)
  • PY_RESUME: throw() 호출을 제외한 Python 함수의 재개(제너레이터 및 코루틴 함수의 경우).
  • PY_THROW: Python 함수가 throw() 호출에 의해 재개됩니다.
  • PY_RETURN: Python 함수로부터의 반환(반환 직전에 발생하며, 피호출자의 프레임이 스택에 있습니다).
  • PY_YIELD: Python 함수에서 yield합니다(yield 직전에 발생하며, 피호출자의 프레임은 스택에 있습니다).
  • PY_UNWIND: 예외 되감기 중에 Python 함수에서 빠져나갑니다.
  • CALL: Python 코드에서의 호출입니다(이벤트는 호출 전에 발생합니다).
  • C_RETURN: Python 함수를 제외한 모든 호출 가능 객체에서 반환합니다(이벤트는 반환 후에 발생합니다).
  • C_RAISE: Python 함수를 제외한 모든 호출 가능 객체에서 예외가 발생합니다(이벤트는 빠져나간 후에 발생합니다).
  • RAISE: 예외가 발생합니다. 단, STOP_ITERATION 이벤트를 일으키는 예외는 제외합니다.
  • EXCEPTION_HANDLED: 예외가 처리됩니다.
  • LINE: 직전 명령어와 줄 번호가 다른 명령어가 곧 실행됩니다.
  • INSTRUCTION – VM 명령어가 곧 실행됩니다.
  • JUMP – 제어 흐름 그래프에서 무조건 점프가 일어납니다.
  • BRANCH – 조건부 브랜치가 선택됩니다(또는 선택되지 않습니다).
  • STOP_ITERATION – 인위적인 StopIteration이 발생합니다. The STOP_ITERATION event를 참조하십시오.

향후 더 많은 이벤트가 추가될 수 있습니다.

모든 이벤트는 sys.monitoring에 있는 events 네임스페이스의 속성이 됩니다. 모든 이벤트는 2의 거듭제곱인 정수로 표현되므로, | 연산자로 조합할 수 있습니다.

이벤트는 세 그룹으로 나뉩니다:

로컬 이벤트

로컬 이벤트는 프로그램의 일반적인 실행과 관련되며, 명확하게 정의된 위치에서 발생합니다. 모든 로컬 이벤트는 비활성화할 수 있습니다. 로컬 이벤트는 다음과 같습니다:

  • PY_START
  • PY_RESUME
  • PY_RETURN
  • PY_YIELD
  • CALL
  • LINE
  • INSTRUCTION
  • JUMP
  • BRANCH (브랜치)
  • STOP_ITERATION

보조 이벤트

보조 이벤트는 다른 이벤트처럼 모니터링할 수 있지만, 다른 이벤트에 의해 제어됩니다:

  • C_RAISE
  • C_RETURN

C_RETURN 및 C_RAISE 이벤트는 CALL 이벤트에 의해 제어됩니다. C_RETURN 및 C_RAISE 이벤트는 대응하는 CALL 이벤트가 모니터링되고 있을 때만 관찰됩니다.

기타 이벤트

기타 이벤트는 반드시 프로그램의 특정 위치에 연결되어 있지는 않으며, 개별적으로 비활성화할 수 없습니다.

모니터링할 수 있는 기타 이벤트는 다음과 같습니다:

  • PY_THROW
  • PY_UNWIND
  • RAISE
  • EXCEPTION_HANDLED

STOP_ITERATION 이벤트

PEP 380에서는 제너레이터나 코루틴에서 값을 반환할 때 StopIteration 예외가 발생한다고 규정합니다. 하지만 이것은 값을 반환하는 매우 비효율적인 방법이므로, 일부 Python 구현, 특히 CPython 3.12+는 다른 코드에서 보이지 않는 한 예외를 발생시키지 않습니다.

도구가 제너레이터와 코루틴을 느리게 하지 않으면서 실제 예외를 모니터링할 수 있도록 STOP_ITERATION 이벤트가 제공됩니다. RAISE와 달리 STOP_ITERATION은 로컬에서 비활성화할 수 있습니다.

도구 식별자

VM은 최대 6개의 도구를 동시에 지원할 수 있습니다. 이벤트를 등록하거나 활성화하기 전에 도구는 식별자를 선택해야 합니다. 식별자는 0에서 5 범위의 정수입니다.

sys.monitoring.use_tool_id(id, name:str) -> None
sys.monitoring.free_tool_id(id) -> None
sys.monitoring.get_tool(id) ->  str | None

sys.monitoring.use_tool_id는 id가 사용 중이면 ValueError를 발생시킵니다. sys.monitoring.get_tool은 id가 사용 중이면 해당 도구의 이름을 반환하고, 그렇지 않으면 None을 반환합니다.

VM은 이벤트와 관련하여 모든 ID를 동일하게 취급하지만, 도구 간 협력을 쉽게 하기 위해 다음 ID가 미리 정의되어 있습니다:

sys.monitoring.DEBUGGER_ID = 0
sys.monitoring.COVERAGE_ID = 1
sys.monitoring.PROFILER_ID = 2
sys.monitoring.OPTIMIZER_ID = 5

ID를 설정할 의무는 없으며, 이미 사용 중인 ID라도 도구가 그 ID를 사용하는 것을 막는 장치도 없습니다. 그러나 도구는 고유한 ID를 사용하고 다른 도구를 존중할 것이 권장됩니다.

예를 들어, 디버거가 연결되어 있고 DEBUGGER_ID가 사용 중이라면, 개의치 않고 계속 진행하는 대신 오류를 보고해야 합니다.

OPTIMIZER_ID는 Python 코드를 최적화하려 하지만 더 넓은 맥락에 따라 무엇을 최적화할지 결정해야 하는 Cinder나 PyTorch 같은 도구를 위해 제공됩니다.

전역적으로 이벤트 설정하기

모니터링 중인 이벤트 집합을 수정하여 이벤트를 전역적으로 제어할 수 있습니다:

  • sys.monitoring.get_events(tool_id:int)->int 활성화된 모든 이벤트를 나타내는 int를 반환합니다.
  • sys.monitoring.set_events(tool_id:int, event_set: int) event_set에 설정된 모든 이벤트를 활성화합니다. tool_id가 사용 중이 아니면 ValueError를 발생시킵니다.

기본적으로 활성화된 이벤트는 없습니다.

코드 객체별 이벤트

이벤트는 코드 객체 단위로도 제어할 수 있습니다:

  • sys.monitoring.get_local_events(tool_id:int, code: CodeType)->int code의 모든 로컬 이벤트를 반환합니다.
  • sys.monitoring.set_local_events(tool_id:int, code: CodeType, event_set: int) code의 로컬 이벤트 중 event_set에 설정된 모든 이벤트를 활성화합니다. tool_id가 사용 중이 아니면 ValueError를 발생시킵니다.

로컬 이벤트는 전역 이벤트에 더해지지만, 전역 이벤트를 가리지는 않습니다. 다시 말해, 로컬 이벤트와 관계없이 모든 전역 이벤트가 코드 객체에 대해 발생합니다.

콜백 함수 등록

이벤트에 호출 가능 객체를 등록하려면 다음을 호출하십시오:

sys.monitoring.register_callback(tool_id:int, event: int, func: Callable | None) -> Callable | None

주어진 tool_id와 event에 대해 다른 콜백이 등록되어 있었다면, 그 콜백은 등록 해제되고 반환됩니다. 그렇지 않으면 register_callback은 None을 반환합니다.

sys.monitoring.register_callback(tool_id, event, None)을 호출하여 함수를 등록 해제할 수 있습니다.

콜백 함수는 언제든지 등록하고 등록 해제할 수 있습니다.

콜백 함수를 등록하거나 등록 해제하면 sys.audit 이벤트가 생성됩니다.

콜백 함수 인자

활성화된 이벤트가 발생하면 등록된 콜백 함수가 호출됩니다. 이벤트마다 다음과 같이 콜백 함수에 서로 다른 인자를 제공합니다.

  • PY_START 및 PY_RESUME:
    func(code: CodeType, instruction_offset: int) -> DISABLE | Any
    
  • PY_RETURN 및 PY_YIELD:
    func(code: CodeType, instruction_offset: int, retval: object) -> DISABLE | Any
  • CALL, C_RAISE 및 C_RETURN:
    func(code: CodeType, instruction_offset: int, callable: object, arg0: object | MISSING) -> DISABLE | Any

    인자가 없으면 arg0은 MISSING으로 설정됩니다.

  • RAISE 및 EXCEPTION_HANDLED:
    func(code: CodeType, instruction_offset: int, exception: BaseException) -> DISABLE | Any
  • LINE:
    func(code: CodeType, line_number: int) -> DISABLE | Any
  • BRANCH:
    func(code: CodeType, instruction_offset: int, destination_offset: int) -> DISABLE | Any

    destination_offset은 코드가 다음에 실행될 위치라는 점에 유의하십시오. 선택되지 않은 브랜치의 경우 이 값은 브랜치 다음에 오는 명령어의 오프셋입니다.

  • INSTRUCTION:
    func(code: CodeType, instruction_offset: int) -> DISABLE | Any

콜백 함수가 DISABLE을 반환하면, sys.monitoring.restart_events()가 호출될 때까지 해당 (code, instruction_offset)에 대해 그 함수는 더 이상 호출되지 않습니다. 이 기능은 커버리지 도구처럼 이벤트를 한 번만 확인하면 되는 도구를 위해 제공됩니다.

sys.monitoring.restart_events()는 특정 도구 하나에만 적용되지 않으므로, 도구는 자신이 DISABLE하기로 한 이벤트를 받을 수 있도록 대비해야 합니다.

콜백 함수 안의 이벤트

콜백을 등록한 도구에 대해서는 그 콜백 함수와 콜백 함수가 호출하는 함수 안에서 이벤트가 일시 중지됩니다.

즉, 한 도구의 콜백 함수 안에서 발생한 이벤트를 다른 도구는 보게 됩니다. 이는 프로파일링 도구를 디버깅할 때 유용할 수 있지만, 디버거 도구가 프로파일에 나타나므로 잘못된 프로파일이 만들어질 수 있습니다.

이벤트 순서

명령어 하나가 여러 이벤트를 발생시키면, 이벤트는 다음 순서로 발생합니다:

  • LINE
  • INSTRUCTION
  • 그 밖의 모든 이벤트(명령어 하나당 이 이벤트 중 하나만 발생할 수 있습니다)

각 이벤트는 ID 오름차순으로 도구에 전달됩니다.

“call” 이벤트 그룹

대부분의 이벤트는 서로 독립적입니다. 한 이벤트를 설정하거나 비활성화해도 다른 이벤트에는 영향을 주지 않습니다. 하지만 CALL, C_RAISE, C_RETURN 이벤트는 하나의 그룹을 이룹니다. 이 중 어떤 이벤트든 설정되거나 비활성화되면, 그룹에 속한 모든 이벤트가 함께 설정되거나 비활성화됩니다. CALL 이벤트를 비활성화해도 그에 대응하는 C_RAISE나 C_RETURN은 비활성화되지 않지만, 그 이후의 모든 이벤트는 비활성화됩니다.

sys.monitoring 네임스페이스의 속성

  • def use_tool_id(id)->None
  • def free_tool_id(id)->None
  • def get_events(tool_id: int)->int
  • def set_events(tool_id: int, event_set: int)->None
  • def get_local_events(tool_id: int, code: CodeType)->int
  • def set_local_events(tool_id: int, code: CodeType, event_set: int)->None
  • def register_callback(tool_id: int, event: int, func: Callable)->Optional[Callable]
  • def restart_events()->None
  • DISABLE: object
  • MISSING: object

“디버그 전용” 기능에 대한 접근

표준 라이브러리의 일부 기능은 일반 코드에서는 접근할 수 없지만 디버거에서는 접근할 수 있습니다. 지역 변수나 줄 번호를 설정하는 기능이 그 예입니다.

이러한 기능은 콜백 함수에서 사용할 수 있습니다.

하위 호환성

이 PEP는 대부분 하위 호환됩니다.

PEP 523 플러그인의 동작은 VM의 제어 범위 밖에 있으므로 PEP 523과 관련된 호환성 문제가 일부 있습니다. 이 PEP의 의미론을 준수하도록 보장하는 것은 PEP 523 플러그인의 몫입니다. VM의 상태를 변경하지 않고 실행을 _PyEval_EvalFrameDefault()에 위임하는 단순한 플러그인은 계속 동작할 것입니다.

sys.settrace()와 sys.setprofile()은 각각 도구 6과 7인 것처럼 동작하므로 이 PEP와 함께 사용할 수 있습니다.

이는 sys.settrace()와 sys.setprofile()이 모든 PEP 523 플러그인에서 올바르게 동작하지 않을 수 있다는 의미입니다. 하지만 위에서 설명한 단순한 PEP 523 플러그인이라면 문제가 없을 것입니다.

성능

활성화된 이벤트가 없다면 이 PEP는 성능에 작지만 긍정적인 영향을 줄 것입니다. 실험 결과 sys.settrace()를 직접 지원하지 않음으로써 1~2%의 속도 향상이 있었습니다.

sys.settrace()의 성능은 거의 같을 것입니다. sys.setprofile()의 성능은 더 좋아질 것입니다. 그러나 sys.settrace()와 sys.setprofile()에 의존하는 도구는 이 PEP가 제공하는 API를 사용하면 훨씬 더 빠르게 만들 수 있습니다.

예를 들어 디버거처럼 소수의 이벤트만 활성화된 경우, 콜백의 오버헤드는 sys.settrace()보다 몇 자릿수 적으며 PEP 523을 사용하는 것보다 훨씬 저렴할 것입니다.

커버리지 도구는 모든 콜백에서 DISABLE을 반환하여 매우 적은 비용으로 구현할 수 있습니다.

예를 들어 LINE을 사용하는 경우처럼 계측이 많은 코드에서는 성능이 sys.settrace보다 좋겠지만, 성능은 콜백에서 소비되는 시간에 좌우되므로 그 차이가 크지는 않을 것입니다.

향후 버전의 CPython(그리고 이 API를 지원하기로 한다면 PyPy도)과 같은 최적화 가상 머신에서는, 오래 실행되는 프로그램 도중에 활성 이벤트 집합을 변경하면 역최적화를 유발하므로 비용이 상당히 클 수 있으며, 수백 밀리초가 걸릴 수도 있습니다. 이러한 역최적화가 일어난 후에는 VM이 계측된 코드를 다시 최적화할 수 있으므로 성능이 회복될 것입니다.

일반적으로 다음 연산은 빠르다고 볼 수 있습니다:

  • def get_events(tool_id: int)->int
  • def get_local_events(tool_id: int, code: CodeType)->int
  • def register_callback(tool_id: int, event: int, func: Callable)->Optional[Callable]
  • def get_tool(tool_id) -> str | None

다음 연산은 더 느리지만, 특별히 느리지는 않습니다:

  • def set_local_events(tool_id: int, code: CodeType, event_set: int)->None

그리고 다음 연산은 느린 것으로 간주해야 합니다:

  • def use_tool_id(id, name:str)->None
  • def free_tool_id(id)->None
  • def set_events(tool_id: int, event_set: int)->None
  • def restart_events()->None

느린 연산이 얼마나 느린지는 그 연산이 언제 일어나는지에 따라 달라집니다. 프로그램 초기, 즉 모듈이 로드되기 전에 수행하면 비용이 꽤 적게 들 것입니다.

메모리 소비

사용하지 않을 때에는 이 PEP가 메모리 소비에 미치는 변화는 무시할 수 있을 정도입니다.

메모리를 어떻게 사용하는지는 전적으로 구현 세부 사항입니다. 그러나 3.12에서 코드 객체당 추가 메모리 소비는 대략 다음과 같을 것으로 예상합니다:

Events
Tools Others LINE INSTRUCTION
One None ≈40% ≈80%
Two or more ≈40% ≈120% ≈200%

보안 영향

실행 중인 코드의 수정을 허용하면 보안에 어느 정도 영향이 있지만, 새로운 코드를 생성하고 호출할 수 있는 능력 이상의 영향은 없습니다.

위에 나열된 새 함수는 모두 감사 훅을 발생시킵니다.

구현

여기에서는 CPython 3.12에 대해 제안된 구현을 개략적으로 설명합니다. 이후 버전의 CPython과 다른 Python 구현의 실제 구현은 상당히 다를 수 있습니다.

이 PEP의 제안된 구현은 PEP 659에 설명된 CPython 3.11의 퀵닝(quickening) 단계를 기반으로 구축됩니다. 계측은 퀵닝과 거의 같은 방식으로 동작하며, 필요에 따라 바이트코드가 계측된 바이트코드로 대체됩니다.

예를 들어 CALL 이벤트가 켜지면 모든 호출 명령어가 INSTRUMENTED_CALL 명령어로 대체됩니다.

이는 특수화를 방해하므로, 등록된 호출 가능 객체를 호출하는 오버헤드에 더해 어느 정도의 성능 저하가 발생한다는 점에 유의하십시오.

활성 이벤트 집합이 변경되면 VM은 모든 스레드의 호출 스택에 있는 모든 코드 객체를 즉시 갱신합니다. 또한 모든 코드 객체가 호출될 때 올바르게 계측되도록 보장하는 트랩을 설치합니다. 따라서 활성 이벤트 집합을 변경하는 작업은 상당히 비용이 큰 연산일 수 있으므로 가능한 한 드물게 수행해야 합니다.

RAISE와 같은 다른 이벤트는 코드 계측이 아니라 기반 이벤트가 발생할 때의 런타임 검사에 의존하므로 적은 비용으로 켜거나 끌 수 있습니다.

계측이 필요한 정확한 이벤트 집합은 구현 세부 사항이지만, 현재 설계에서는 다음 이벤트에 계측이 필요합니다:

  • PY_START
  • PY_RESUME
  • PY_RETURN
  • PY_YIELD
  • CALL
  • LINE
  • INSTRUCTION
  • JUMP
  • BRANCH (브랜치)

계측된 각 바이트코드에는 해당 계측이 어느 도구에 적용되는지 기록하기 위해 추가로 8비트의 정보가 필요합니다. LINE 및 INSTRUCTION 이벤트는 원래 명령어를 저장해야 하고, 다른 계측과 겹치는 경우에는 계측된 명령어까지 저장해야 하므로 추가 정보가 필요합니다.

도구 구현하기

이 PEP의 철학은 서드파티 모니터링 도구가 높은 성능을 달성할 수 있어야 한다는 것이지, 그렇게 하기가 쉬워야 한다는 것이 아닙니다.

이벤트를 사용자에게 의미 있는 데이터로 변환하는 것은 도구의 책임입니다.

모든 이벤트에는 비용이 따르므로, 도구는 필요한 정보를 제공하면서도 가장 드물게 발생하는 이벤트 집합을 사용하도록 노력해야 합니다.

디버거

중단점 삽입

중단점은 코드 객체별 이벤트로 LINE 또는 INSTRUCTION을 설정하고, 중단점과 일치하지 않는 모든 이벤트에 대해 DISABLE을 반환하는 방식으로 삽입할 수 있습니다.

단계 실행

디버거는 보통 명령어 하나 또는 한 줄 단위로 실행을 진행하는 기능을 제공합니다.

중단점과 마찬가지로, 단계 실행은 코드 객체별 이벤트를 설정하여 구현할 수 있습니다. 일반 실행을 재개할 때가 되면 즉시 로컬 이벤트 설정을 해제할 수 있습니다.

연결

디버거는 PY_START 및 PY_RESUME 이벤트를 사용하여 코드 객체를 처음 만났을 때 알림을 받을 수 있으므로, 필요한 중단점을 삽입할 수 있습니다.

커버리지 도구

커버리지 도구는 제어 그래프의 어느 부분이 실행되었는지 추적해야 합니다. 이를 위해 커버리지 도구는 PY_ 이벤트와 더불어 JUMP 및 BRANCH 이벤트에 등록해야 합니다.

이 정보는 실행이 완료된 후 다시 줄 기반 보고서로 변환할 수 있습니다.

프로파일러

단순한 프로파일러는 호출에 대한 정보를 수집해야 합니다. 이를 위해 프로파일러는 다음 이벤트에 등록해야 합니다:

  • PY_START
  • PY_RESUME
  • PY_THROW
  • PY_RETURN
  • PY_YIELD
  • PY_UNWIND
  • CALL
  • C_RAISE
  • C_RETURN

줄 기반 프로파일러

줄 기반 프로파일러는 LINE 및 JUMP 이벤트를 사용할 수 있습니다. 프로파일러를 구현하는 사람은 LINE 이벤트를 계측하면 성능에 큰 영향을 준다는 점을 알고 있어야 합니다.

Note

계측(instrumenting) 프로파일러는 오버헤드가 상당히 크며 프로파일링 결과를 왜곡합니다. 정확한 호출 횟수가 필요한 경우가 아니라면 통계적 프로파일러 사용을 고려하십시오.

기각된 아이디어

이 PEP의 초안 버전에서는 VM이 모니터링 명령어를 삽입하는 대신 사용자가 직접 삽입할 책임을 지도록 하는 방안을 제안했습니다. 그러나 이 방식은 도구에 지나치게 큰 부담을 주며, 디버거를 연결하는 것을 거의 불가능하게 만들 것입니다.

이 PEP의 이전 버전에서는 이벤트를 enums로 저장하는 방안을 제안했습니다:

class Event(enum.IntFlag):
    PY_START = ...

그러나 그렇게 하면 enum 모듈이 로드되기 전의 코드는 모니터링할 수 없게 되고, 불필요한 오버헤드가 발생할 수 있습니다.