PEP 573 – C 확장 메서드에서 모듈 상태에 접근하기
- Author:
- Petr Viktorin <encukou at gmail.com>, Alyssa Coghlan <ncoghlan at gmail.com>, Eric Snow <ericsnowcurrently at gmail.com>, Marcel Plch <gmarcel.plch at gmail.com>
- BDFL-Delegate:
- Stefan Behnel
- Discussions-To:
- Import-SIG list
- Status:
- Final
- Type:
- Standards Track
- Created:
- 02-Jun-2016
- Python-Version:
- 3.9
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 CPython 확장 메서드가 자신이 정의된 모듈의 상태와 같은 컨텍스트에 접근할 수 있는 방법을 추가할 것을 제안합니다.
이를 통해 확장 메서드는 모듈 상태를 조회할 때 PyState_FindModule 대신 직접 포인터 역참조를 사용할 수 있으며, 프로세스 전역 상태보다 모듈 범위 상태를 사용할 때 발생하는 성능 비용을 줄이거나 제거할 수 있습니다.
이는 PEP 3121 (확장 모듈 초기화 및 종료) 및 PEP 489 (다단계 확장 모듈 초기화)의 도입을 가로막는 남은 장애물 중 하나를 해결합니다.
이 PEP는 PEP 3121 및 PEP 489가 해결하기 시작한 문제를 완전히 해결하는 데 추가적인 진전을 이루지만, 남아 있는 모든 우려 사항을 해결하려고 하지는 않습니다. 특히 슬롯 메서드(nb_add, 등)에서 모듈 상태에 접근하는 문제는 해결되지 않습니다.
용어
프로세스 전역 상태
C 수준의 정적 변수입니다. 이는 매우 저수준의 메모리 저장소이므로 신중하게 관리해야 합니다.
모듈별 상태
모듈 객체에 로컬인 상태이며, 모듈 객체를 초기화하는 과정의 일부로 동적으로 할당됩니다. 이를 통해 다른 서브인터프리터에 있는 모듈을 포함하여 모듈의 다른 인스턴스와 상태를 격리합니다.
PyModule_GetState()를 통해 접근합니다.
정적 타입
C 수준의 정적 변수로 정의된 타입 객체, 즉 컴파일에 내장된 타입 객체입니다.
정적 타입은 모듈 인스턴스 간에 공유되어야 하며 자신이 어느 모듈에 속하는지에 대한 정보가 없습니다. 정적 타입에는 __dict__가 없습니다(단, 해당 인스턴스에는 있을 수 있습니다).
힙 타입
실행 시간에 생성되는 타입 객체입니다.
정의 클래스
메서드(바운드 또는 언바운드)가 정의된 클래스가 해당 메서드의 정의 클래스입니다. 단순히 기반 클래스에서 메서드를 상속하는 클래스는 정의 클래스가 아닙니다.
예를 들어, int는 True.to_bytes, True.__floor__ 및 int.__repr__의 정의 클래스입니다.
C에서는 해당 tp_methods또는 “tp slots” [1] 항목으로 정의된 클래스가 정의 클래스입니다. Python에서 정의된 메서드의 경우 정의 클래스는 __class__ 클로저 셀에 저장됩니다.
C API
Python 문서에서 설명하는 “Python/C API”입니다. CPython은 C API를 구현하지만 다른 구현도 존재합니다.
근거
PEP 489는 확장 모듈을 초기화하는 새로운 방법을 도입했으며, 이를 구현하는 확장 모듈에 여러 이점을 제공합니다:
- 확장 모듈은 Python 대응 모듈과 더 비슷하게 동작합니다.
- 확장 모듈은 기존 모듈 객체에 쉽게 로드할 수 있으므로,
runpy에 대한 확장 모듈 지원이나 확장 모듈 다시 로드를 가능하게 하는 시스템을 위한 기반이 마련됩니다. - 동일한 확장에서 여러 모듈을 로드할 수 있으므로, 단일 인터프리터에서 모듈 격리(적절한 서브인터프리터 지원을 위한 핵심 기능)를 테스트할 수 있습니다.
관련 PEP 489의 채택을 가로막는 가장 큰 장애물은 확장 타입의 메서드에서 모듈 상태에 접근할 수 있도록 하는 것입니다. 현재 확장 메서드에서 이 상태에 접근하는 방법은 PyState_FindModule을 통해 모듈을 조회하는 것입니다(확장 모듈의 모듈 수준 함수가 인자로 모듈 참조를 받는 것과는 대조적입니다). 그러나 PyState_FindModule은 스레드 로컬 상태를 조회하므로 C 수준 프로세스 전역 접근에 비해 상대적으로 비용이 많이 들며, 결과적으로 모듈 작성자가 이를 사용하지 않게 만듭니다.
또한 PyState_FindModule은 각 서브인터프리터에 주어진 PyModuleDef에 대응하는 모듈이 최대 하나만 존재한다는 가정에 의존합니다. 이 가정은 PEP 489의 다단계 초기화를 사용하는 모듈에는 성립하지 않으므로, 이러한 모듈에서는 PyState_FindModule을 사용할 수 없습니다.
확장 메서드에서 모듈 수준 상태에 접근할 수 있는 더 빠르고 안전한 방법이 필요합니다.
배경입니다.
Python 메서드의 구현에서는 다음 정보 중 하나 이상에 접근해야 할 수 있습니다.
- 메서드가 호출된 인스턴스(
self) - 기반 함수
- 정의 클래스, 즉 메서드가 정의된 클래스
- 해당 모듈
- 모듈 상태
Python 코드에서는 Python 수준의 대응 항목을 다음과 같이 가져올 수 있습니다.:
import sys
class Foo:
def meth(self):
instance = self
module_globals = globals()
module_object = sys.modules[__name__] # (1)
underlying_function = Foo.meth # (1)
defining_class = Foo # (1)
defining_class = __class__ # (2)
Note
정의 클래스는 type(self)가 아닙니다. type(self)가 Foo의 서브클래스일 수 있기 때문입니다.
(1)로 표시된 문은 함수의 __globals__를 통한 이름 기반 조회에 암묵적으로 의존합니다. 즉, 정의 클래스와 Python 함수 객체에 접근하기 위한 Foo속성 또는 sys.modules에서 모듈 객체를 찾기 위한 __name__에 의존합니다.
Python 코드에서는 함수 정의가 실행될 때 __globals__가 적절하게 설정되므로 이것이 가능합니다. 또한 네임스페이스가 조작되어 다른 객체를 반환하더라도 최악의 경우 예외가 발생할 뿐입니다.
__class__클로저 (2)는 정의 클래스를 가져오는 더 안전한 방법이지만, 여전히 __closure__가 적절하게 설정되어 있어야 합니다.
이와 대조적으로 확장 메서드는 일반적으로 일반 C 함수로 구현됩니다. 이는 확장 메서드가 인자와 C 수준의 스레드 로컬 및 프로세스 전역 상태에만 접근할 수 있음을 의미합니다. 전통적으로 많은 확장 모듈은 공유 상태를 C 수준의 프로세스 전역 변수에 저장해 왔으며, 이로 인해 다음과 같은 문제가 발생했습니다.
- 동일한 프로세스에서 여러 초기화/종료 주기를 실행하는 경우
- 모듈을 다시 로드하는 경우(예: 조건부 임포트를 테스트하기 위해)
- 서브인터프리터에서 확장 모듈을 로드하는 경우
PEP 3121은 PyState_FindModuleAPI를 제공하여 이 문제를 해결하려 했지만, 확장 메서드와 관련해서는(모듈 수준 함수와는 달리) 여전히 중대한 문제가 있습니다.
- C 수준 프로세스 전역 상태에 직접 접근하는 것보다 현저히 느립니다.
- 여전히 프로세스 전역 상태에 본질적으로 의존하므로 모듈 다시 로드를 안정적으로 처리하지 못합니다.
또한 모듈 상태와 같은 C 수준 구조체를 조회할 때 예상치 못한 객체 레이아웃을 제공하면 인터프리터가 충돌할 수 있으므로, 확장 메서드가 예상하는 종류의 객체를 받도록 보장하는 것이 훨씬 더 중요합니다.
제안입니다.
현재 바인딩된 확장 메서드(PyCFunction 또는 PyCFunctionWithKeywords)는 self와 제공된 위치 인자 및 키워드 인자(해당하는 경우)만 받습니다.
모듈 수준 확장 함수는 이미 self 인자를 통해 정의 모듈 객체에 접근할 수 있지만, 확장 타입의 메서드에는 그러한 이점이 없습니다. 확장 타입의 메서드는 self를 통해 바인딩된 인스턴스를 받으므로 정의 클래스나 모듈 수준 상태에 직접 접근할 수 없습니다.
위에서 설명한 추가 모듈 수준 컨텍스트는 두 가지 변경을 통해 사용할 수 있게 할 수 있습니다. 두 추가 사항은 모두 선택 사항이며, 확장 작성자는 이를 사용하려면 명시적으로 선택해야 합니다:
- 힙 타입 객체에 모듈 포인터를 추가하십시오.
- 기반 C 함수에 정의 클래스를 전달하십시오.
CPython에서는 내장 메서드 객체(
PyCFunctionObject)가 생성될 때 정의 클래스를 쉽게 이용할 수 있으므로,PyCFunctionObject를 확장하는 새 구조체에 저장할 수 있습니다.
그러면 모듈 객체에서 PyModule_GetState를 통해 모듈 상태를 가져올 수 있습니다.
이 제안에 따르면 메서드가 Per-module State에 접근해야 하는 모든 타입은 정적 타입이 아니라 힙 타입이어야 합니다. 이는 하나의 확장에서 여러 모듈 객체를 로드할 수 있도록 하는 데 필요합니다. C 수준 전역인 정적 타입에는 자신이 어느 모듈 객체에 속하는지에 대한 정보가 없기 때문입니다.
슬롯 메서드
위의 변경 사항은 tp_iter 또는 nb_add와 같은 슬롯 메서드를 다루지 않습니다.
슬롯 메서드의 문제는 해당 C API가 고정되어 있으므로 정의 클래스를 전달하기 위한 새 인자를 간단히 추가할 수 없다는 점입니다. 이 문제에 대해서는 두 가지 가능한 해결책이 제안되었습니다:
- MRO를 순회하여 클래스를 조회하십시오. 이는 잠재적으로 비용이 많이 들지만, 성능이 문제가 되지 않는 경우(예를 들어 모듈 수준 예외를 발생시킬 때) 사용할 수 있습니다.
- 별도 테이블인
__typeslots__[2]에 각 슬롯의 정의 클래스를 가리키는 포인터를 저장하십시오. 이는 기술적으로 실현 가능하고 빠르지만, 변경 범위가 상당히 큽니다.
이 문제의 영향을 받는 모듈은 thread-local state또는 PEP 567 context variables를 캐싱 메커니즘으로 사용할 수도 있고, 자체적으로 다시 로드에 친화적인 조회 캐싱 방식을 정의할 수도 있습니다.
이 문제를 일반적으로 해결하는 일은 향후 PEP로 미룹니다.
사양
힙 타입에 모듈 참조 추가
모듈을 생성하기 위한 새 팩토리 메서드가 C-API에 추가됩니다.:
PyObject* PyType_FromModuleAndSpec(PyObject *module,
PyType_Spec *spec,
PyObject *bases)
이는 PyType_FromSpecWithBases와 동일하게 작동하며, 추가로 제공된 모듈 객체를 새 타입과 연결합니다. (CPython에서는 아래에 설명된 ht_module을 설정합니다.)
또한 PyObject * PyType_GetModule(PyTypeObject *)가 액세서로 제공됩니다. 설정된 경우 타입에 연결된 모듈을 반환하며, 그렇지 않으면 TypeError를 설정하고 NULL을 반환합니다. 정적 타입이 주어지면 항상 TypeError를 설정하고 NULL을 반환합니다.
CPython에서 이를 구현하기 위해 PyHeapTypeObject 구조체에는 연결된 모듈에 대한 포인터를 저장하는 새 멤버 PyObject *ht_module를 추가합니다. 기본값은 NULL이며 타입 객체가 생성된 후에는 수정하지 않아야 합니다.
ht_module 멤버는 서브클래스에 상속되지 않습니다. 이 멤버가 필요한 각각의 개별 타입에 대해 PyType_FromSpecWithBases를 사용하여 설정해야 합니다.
일반적으로 ht_module이 설정된 클래스를 생성하면 클래스와 모듈이 관련된 참조 순환이 생성됩니다. 모듈을 정리하는 작업은 성능에 민감한 작업이 아니며 모듈 수준 함수도 일반적으로 참조 순환을 생성하므로, 이는 문제가 되지 않습니다. f_globals를 통한 함수 순환을 끊는 기존의 “모든 모듈 전역 변수를 None으로 설정”하는 코드가 ht_module을 통한 새로운 순환도 끊습니다.
확장 메서드에 정의 클래스를 전달하기
PyMethodDef.ml_flags에서 사용하기 위한 새 시그니처 플래그인 METH_METHOD이 추가됩니다. 개념적으로 이는 함수 시그니처에 defining_class를 추가합니다. 초기 구현을 쉽게 하기 위해 이 플래그는 (METH_FASTCALL | METH_KEYWORDS | METH_METHOD)로만 사용할 수 있습니다. (METH_O와 같은 다른 플래그나 단독 METH_FASTCALL과는 함께 사용할 수 없지만, METH_CLASS또는 METH_STATIC과는 함께 사용할 수 있습니다.)
이 플래그 조합을 사용하여 정의된 메서드의 C 함수는 PyCMethod라는 새 C 시그니처를 사용하여 호출됩니다.:
PyObject *PyCMethod(PyObject *self,
PyTypeObject *defining_class,
PyObject *const *args,
size_t nargsf,
PyObject *kwnames)
(METH_VARARGS | METH_METHOD)와 같은 추가 조합이 향후에 추가될 수 있습니다(또는 이 PEP의 초기 구현에 포함될 수도 있습니다). 그러나 METH_METHOD은 항상 additional 플래그여야 합니다. 즉, 필요한 경우에만 정의 클래스를 전달해야 합니다.
CPython에는 추가 정보를 저장하기 위해 PyCFunctionObject를 확장하는 새 구조체가 추가됩니다.:
typedef struct {
PyCFunctionObject func;
PyTypeObject *mm_class; /* Passed as 'defining_class' arg to the C func */
} PyCMethodObject;
PyCFunction구현은 METH_METHOD 플래그가 설정된 것을 발견하면 mm_class를 PyCMethod C 함수에 전달합니다. mm_class에 더 쉽게 접근할 수 있도록 새 매크로 PyCFunction_GET_CLASS(cls)가 추가됩니다.
C 메서드는 정의 클래스나 모듈에 접근할 필요가 없다면 다른 METH_* 시그니처를 계속 사용할 수 있습니다. METH_METHOD이 설정되지 않은 경우 PyCMethodObject로 캐스팅하는 것은 유효하지 않습니다.
Argument Clinic
Argument Clinic을 사용하는 메서드에 정의 클래스를 전달할 수 있도록 CPython의 Argument Clinic 도구에 defining_class라는 새 변환기가 추가됩니다.
각 메서드는 이 변환기를 사용하는 인자를 하나만 가질 수 있으며, 해당 인자는 self뒤에 나타나야 합니다. self를 사용하지 않는 경우에는 첫 번째 인자여야 합니다. 해당 인자의 타입은 PyTypeObject *입니다.
이를 사용하면 Argument Clinic은 METH_FASTCALL | METH_KEYWORDS | METH_METHOD을 호출 규약으로 선택합니다. 해당 인자는 __text_signature__에 나타나지 않습니다.
새 변환기는 처음에는 __init__ 및 __new__ 메서드와 호환되지 않습니다. 이러한 메서드는 METH_METHOD규약을 사용할 수 없습니다.
헬퍼
힙 타입에서 Per-module State에 접근하는 것은 매우 일반적인 작업입니다. 이를 쉽게 하기 위한 헬퍼가 추가됩니다.:
void *PyType_GetModuleState(PyObject *type)
이 함수는 힙 타입을 받아 성공하면 해당 힙 타입이 속한 모듈의 상태를 가리키는 포인터를 반환합니다.
실패할 경우 두 가지 상황이 발생할 수 있습니다. 타입이 아닌 객체나 모듈이 없는 타입이 전달되면 TypeError가 설정되고 NULL이 반환됩니다. 모듈을 찾으면 NULL일 수도 있는 상태 포인터가 예외를 설정하지 않고 반환됩니다.
초기 구현에서 변환되는 모듈
이 접근 방식을 검증하기 위해 초기 구현 중에 _elementtree 모듈이 수정됩니다.
API 변경 및 추가 사항 요약
다음이 Python C-API에 추가될 예정입니다:
PyType_FromModuleAndSpec함수PyType_GetModule함수PyType_GetModuleState함수METH_METHOD호출 플래그PyCMethod함수 시그니처
다음 추가 사항은 CPython 구현 세부 사항으로 추가되며, 문서화되지 않을 것입니다:
PyCFunction_GET_CLASS매크로PyCMethodObject구조체_heaptypeobject의ht_module멤버- Argument Clinic의
defining_class변환기
하위 호환성
모든 힙 타입에 새 포인터 하나가 추가됩니다. 다른 모든 변경 사항은 새 함수와 구조체를 추가하거나, 비공개 구현 세부 사항을 변경하는 것입니다.
구현
향후 가능한 확장
슬롯 메서드
슬롯 메서드에 정의 클래스(또는 모듈 상태)를 전달하는 방법이 향후 추가될 수 있습니다.
이 PEP의 이전 버전에서는 특정 함수에 슬롯을 정의하는 클래스를 MRO에서 검색하여 정의 클래스를 결정하는 헬퍼 함수를 제안했습니다. 그러나 클래스가 변경되면(힙 타입의 경우 Python 코드에서 가능함) 이 접근 방식은 실패합니다. 이 문제를 해결하는 것은 향후 논의로 남겨둡니다.
모듈 참조를 사용한 타입의 쉬운 생성
PyType_FromModuleAndSpec를 호출하는 것보다 힙 타입 생성을 훨씬 쉽게 만들기 위해 PEP 489 실행 슬롯 타입을 추가하는 것이 가능할 것입니다. 이것은 향후 PEP로 남겨둡니다.
제한된 API에서 정적 예외 타입을 생성하는 좋은 방법을 추가하는 것이 좋을 수 있습니다. 이러한 예외 타입은 서브인터프리터 간에 공유될 수 있지만, 특정 모듈 상태 없이 인스턴스화될 수 있습니다. 이것 역시 향후 가능한 논의로 남겨둡니다.
최적화
여기서 제안된 대로, METH_METHOD 플래그로 정의된 메서드는 하나의 특정 시그니처만 지원합니다.
성능상의 이유로 다른 시그니처가 필요한 것으로 밝혀지면, 추가될 수 있습니다.
참고 문헌
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.