PEP 620 – C API에서 구현 세부 사항 숨기기
- Author:
- Victor Stinner <vstinner at python.org>
- Status:
- Withdrawn
- Type:
- Standards Track
- Created:
- 19-Jun-2020
- Python-Version:
- 3.12
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
구현 세부 사항을 숨기기 위해 C API에 호환되지 않는 변경을 도입합니다.
대부분의 구현 세부 사항이 숨겨지면 CPython 내부의 발전은 C API 하위 호환성 문제에 덜 제약받게 됩니다. 새로운 기능을 추가하기가 훨씬 쉬워질 것입니다.
태그된 포인터처럼 단순한 미세 최적화를 넘어서는 더 고급 최적화를 CPython에서 실험할 수 있게 됩니다.
동작하지 않게 되는 C 확장의 수를 줄이기 위한 프로세스를 정의합니다.
이 PEP의 구현은 여러 Python 버전에 걸쳐 신중하게 진행될 예정입니다. 구현은 이미 Python 3.7에서 시작되었으며 대부분의 변경은 이미 완료되었습니다. Process to reduce the number of broken C extensions가 그 진행 속도를 결정합니다.
PEP 철회
이 PEP는 범위가 너무 넓고 작업이 여러 Python 버전에 걸쳐 분산되어 있어 PEP 전체에 대한 결정을 내리기 어렵기 때문에 작성자가 철회했습니다. 이 PEP는 범위가 더 좁고 더 명확하게 정의된 새 PEP들(예: PEP 670)로 분할되었습니다.
동기
C API가 CPython의 발전을 가로막습니다
C 구조체의 멤버를 추가하거나 제거하면 여러 하위 호환성 문제가 발생합니다.
새 멤버를 추가하면 안정 ABI(PEP 384)가 깨지며, 정적으로 선언된 타입(예: static PyTypeObject MyType = {...};)에서 특히 그렇습니다. Python 3.4에서 PEP 442 “안전한 객체 종료 처리”가 PyTypeObject 구조체 끝에 tp_finalize 멤버를 추가했습니다. ABI 하위 호환성을 위해, 타입 구조체에 tp_finalize 멤버가 포함되어 있는지 알리는 새로운 Py_TPFLAGS_HAVE_FINALIZE 타입 플래그가 필요했습니다. 이 플래그는 Python 3.8에서 제거되었습니다(bpo-32388).
2009년에 출시된 Python 3.0부터 폐지 예정이었던 PyTypeObject.tp_print 멤버는 Python 3.8 개발 주기에 제거되었습니다. 그러나 이 변경은 너무 많은 C 확장을 동작하지 않게 만들어 3.8 최종 릴리스 전에 되돌려야 했습니다. 결국 이 멤버는 Python 3.9에서 다시 제거되었습니다.
C 확장은 C API를 통해 간접적으로, 또는 심지어 직접적으로 구조체 멤버에 접근할 수 있다는 점에 의존합니다. PyListObject 같은 구조체를 수정하는 것은 고려조차 할 수 없습니다.
PyTypeObject 구조체는 가장 많이 발전한 구조체인데, 이는 단순히 이를 수정하는 것 외에는 CPython을 발전시킬 다른 방법이 없었기 때문입니다.
C 확장은 기술적으로 PyObject* 포인터를 역참조하여 PyObject 멤버에 접근할 수 있습니다. 이 때문에 태그된 포인터(유효한 PyObject 구조체를 가리키지 않는 PyObject*로 작은 값을 저장하는 것) 같은 실험이 불가능합니다.
Python 가비지 컬렉터를 추적 가비지 컬렉터로 교체하려면 PyObject.ob_refcnt 참조 카운터도 제거해야 하는데, 현재 Py_INCREF() 및 Py_DECREF() 매크로는 PyObject.ob_refcnt에 직접 접근합니다.
1990년 이후 변함없는 CPython 설계: 구조체와 참조 카운팅
CPython 프로젝트는 처음 만들어질 때 한 명의 개발자가 유지 보수할 수 있을 만큼 구현을 단순하게 유지한다는 한 가지 원칙에 따라 작성되었습니다. CPython의 복잡도는 크게 증가했고 많은 미세 최적화가 구현되었지만, CPython의 핵심 설계는 바뀌지 않았습니다.
PyObject 및 PyTupleObject 구조체의 멤버는 “Initial revision” 커밋(1990) 이후로 바뀌지 않았습니다:
#define OB_HEAD \
unsigned int ob_refcnt; \
struct _typeobject *ob_type;
typedef struct _object {
OB_HEAD
} object;
typedef struct {
OB_VARHEAD
object *ob_item[1];
} tupleobject;
이름만 바뀌었습니다. object는 PyObject로, tupleobject는 PyTupleObject로 이름이 바뀌었습니다.
CPython은 내부적으로, 그리고 서드파티 C 확장을 위해 (Python C API를 통해) 여전히 참조 카운팅을 사용하여 Python 객체의 수명을 추적합니다.
모든 Python 객체는 힙에 할당되어야 하며 이동할 수 없습니다.
PyPy가 CPython보다 효율적인 이유는 무엇입니까?
PyPy 프로젝트는 평균적으로 CPython보다 4.2배 빠른 Python 구현입니다. PyPy 개발자들은 최적화 방식을 더 자유롭게 선택하기 위해 CPython을 포크하지 않고 처음부터 새로 시작하기로 했습니다.
PyPy는 참조 카운팅을 사용하지 않고, 객체를 이동시키는 추적 가비지 컬렉터를 사용합니다. 객체를 항상 힙에 할당할 필요 없이 스택에 할당할 수 있습니다(아예 할당하지 않을 수도 있습니다).
객체 레이아웃은 성능을 고려하여 설계되었습니다. 예를 들어, 리스트 전략은 정수를 객체가 아닌 정수 그대로 직접 저장합니다.
또한 PyPy에는 효율적인 PyPy 설계 덕분에 빠른 코드를 생성하는 JIT 컴파일러도 있습니다.
PyPy의 병목: Python C API
PyPy는 순수 Python 코드를 실행할 때는 CPython보다 훨씬 효율적이지만, C 확장을 실행할 때는 CPython과 비슷하거나 더 느립니다.
C API는 PyObject*를 요구하며 구조체 멤버에 직접 접근하는 것을 허용하므로, PyPy는 PyPy 객체에 CPython 객체를 연결하고 둘의 일관성을 유지해야 합니다. PyPy 객체를 CPython 객체로 변환하는 작업은 비효율적입니다. 또한 참조 카운팅도 PyPy의 추적 가비지 컬렉터 위에 구현해야 합니다.
이러한 변환이 필요한 이유는 Python C API가 CPython 구현에 너무 밀접하게 붙어 있어 고수준 추상화가 없기 때문입니다. 예를 들어, 구조체 멤버는 공개 C API의 일부이며, C 확장이 PyTupleObject.ob_item[0](튜플의 첫 번째 항목)을 직접 읽거나 설정하는 것을 막는 장치가 전혀 없습니다.
자세한 내용은 Antonio Cuni가 작성한 Inside cpyext: Why emulating CPython C API is so Hard (2018년 9월)를 참고하십시오.
근거
구현 세부 사항 숨기기
C API에서 구현 세부 사항을 숨기면 여러 가지 이점이 있습니다:
- CPython에서 단순한 마이크로 최적화를 넘어 더 발전된 최적화를 실험할 수 있게 됩니다. 예를 들어, 태그된 포인터(tagged pointer)를 도입하거나, 가비지 컬렉터를 객체를 이동할 수 있는 추적 가비지 컬렉터로 교체할 수 있습니다.
- CPython에 새로운 기능을 추가하기가 더 쉬워집니다.
- PyPy는 더 많은 경우에 CPython 객체로의 변환을 피하고 효율적인 PyPy 객체를 그대로 유지할 수 있게 됩니다.
- 새로운 Python 구현을 위해 C API를 구현하기가 더 쉬워집니다.
- 더 많은 C 확장이 CPython 이외의 Python 구현과 호환될 것입니다.
제한된 C API와의 관계
PEP 384 “안정 ABI 정의”는 Python 3.4에 구현되었습니다. 이 PEP는 C API의 부분 집합인 “제한된 C API”를 도입합니다. 제한된 C API를 사용하면 C 확장을 한 번만 빌드하여 여러 Python 버전에서 사용할 수 있습니다. 이것이 안정 ABI입니다.
PEP 384의 주요 한계는 C 확장이 제한된 C API 사용을 명시적으로 선택해야 한다는 점입니다. 이를 선택한 프로젝트는 극소수에 불과하며, 대개 바이너리 배포, 특히 Windows에서의 배포를 쉽게 하기 위해서였습니다.
이 PEP는 C API를 제한된 C API 쪽으로 옮겨 갑니다.
이상적으로는 C API가 제한된 C API가 되고 모든 C 확장이 안정 ABI를 사용하게 되겠지만, 이는 이 PEP의 범위를 벗어납니다.
명세
요약
- (완료) C API 헤더 파일을 재구성합니다.
Include/cpython/및Include/internal/하위 디렉터리를 만듭니다. - (완료) 구현 세부 사항을 노출하는 비공개 함수를 내부 C API로 옮깁니다.
- (완료) 매크로를 정적 인라인 함수로 변환합니다.
- (완료) 새 함수
Py_SET_TYPE(),Py_SET_REFCNT(),Py_SET_SIZE()를 추가합니다.Py_TYPE(),Py_REFCNT(),Py_SIZE()매크로는 l-value로 사용할 수 없는 함수가 됩니다. - (완료) 새 C API 함수는 빌린 참조를 반환해서는 안 됩니다.
- (진행 중)
pythoncapi_compat.h헤더 파일을 제공합니다. - (진행 중) 구조체를 불투명하게 만들고 getter 및 setter 함수를 추가합니다.
- (시작 전)
PySequence_Fast_ITEMS()를 폐지 예정으로 지정합니다. - (시작 전)
PyTuple_GET_ITEM()및PyList_GET_ITEM()매크로를 정적 인라인 함수로 변환합니다.
C API 헤더 파일 재구성
C API의 첫 번째 사용자는 Python 자신이었습니다. Python 외부에서 사용해서는 안 되는 API와 의도적으로 공개된 API 사이에 명확한 구분이 없습니다.
헤더 파일은 3가지 API로 재구성해야 합니다:
Include/디렉터리는 제한된 C API입니다. 구현 세부 사항이 없으며 구조체는 불투명합니다. 이를 사용하는 C 확장은 안정 ABI를 얻습니다.Include/cpython/디렉터리는 CPython C API입니다. “이식성”이 더 낮은 API로, 파이썬 버전에 더 많이 의존하고 일부 구현 세부 사항을 노출하며, 호환되지 않는 변경이 약간 있을 수 있습니다.Include/internal/디렉터리는 내부 C API입니다. 구현 세부 사항이며, Python 릴리스마다 호환되지 않는 변경이 있을 가능성이 높습니다.
Include/cpython/ 디렉터리의 생성은 하위 호환성을 완전히 유지합니다. Include/cpython/ 헤더 파일은 직접 포함할 수 없으며, Py_LIMITED_API 매크로가 정의되지 않았을 때 Include/ 헤더 파일에 의해 자동으로 포함됩니다.
내부 C API는 설치되며, 코드를 실행하지 않고 구조체 멤버에 접근해야 하는 디버거나 프로파일러 같은 특수한 용도로 사용할 수 있습니다. 내부 C API를 사용하는 C 확장은 Python 버전에 강하게 결합되어 있으므로 Python 버전마다 다시 컴파일해야 합니다.
STATUS: 완료됨 (Python 3.8에서)
헤더 파일 재구성은 Python 3.7에서 시작되어 Python 3.8에서 완료되었습니다.
비공개 함수를 내부 C API로 이동
구현 세부 사항을 노출하는 비공개 함수는 내부 C API로 옮겨야 합니다.
C 확장이 CPython 구현 세부 사항을 노출하는 CPython 비공개 함수에 의존하면, 다른 Python 구현은 이 C 확장을 지원하기 위해 해당 비공개 함수를 다시 구현해야 합니다.
상태: 완료 (Python 3.9에서)
Python 3.8에서 내부 C API로 옮긴 비공개 함수:
_PyObject_GC_TRACK(),_PyObject_GC_UNTRACK()
Python 3.9에서 제한된 C API에서 제외한 매크로와 함수:
_PyObject_SIZE(),_PyObject_VAR_SIZE()PyThreadState_DeleteCurrent()PyFPE_START_PROTECT(),PyFPE_END_PROTECT()_Py_NewReference(),_Py_ForgetReference()_PyTraceMalloc_NewReference()_Py_GetRefTotal()
Python 3.9에서 내부 C API로 옮긴 비공개 함수:
_Py_AS_GC(),_PyObject_GC_IS_TRACKED(),_PyGCHead_NEXT()같은 GC 함수_Py_AddToAllObjects()(익스포트되지 않음)_PyDebug_PrintTotalRefs(),_Py_PrintReferences(),_Py_PrintReferenceAddresses()(익스포트되지 않음)
Python 3.9에서 내부 C API로 옮기고 비공개 함수로 이름을 바꾼 공개 “자유 목록 비우기(clear free list)” 함수:
PyAsyncGen_ClearFreeLists()PyContext_ClearFreeList()PyDict_ClearFreeList()PyFloat_ClearFreeList()PyFrame_ClearFreeList()PyList_ClearFreeList()PyTuple_ClearFreeList()- 그냥 제거한 함수:
PyMethod_ClearFreeList()와PyCFunction_ClearFreeList(): 바운드 메서드 자유 목록은 Python 3.9에서 제거되었습니다.PySet_ClearFreeList(): 집합 자유 목록은 Python 3.4에서 제거되었습니다.PyUnicode_ClearFreeList(): 유니코드 자유 목록은 Python 3.3에서 제거되었습니다.
매크로를 정적 인라인 함수로 변환
매크로를 정적 인라인 함수로 변환하면 여러 가지 이점이 있습니다:
- 함수는 매개변수 타입과 반환 타입이 명확하게 정의되어 있습니다.
- 함수는 스코프(해당 함수)가 명확하게 정의된 변수를 사용할 수 있습니다.
- 디버거는 함수에 중단점을 설정할 수 있고, 프로파일러는 호출 스택에 함수 이름을 표시할 수 있습니다. 대부분의 경우, 정적 인라인 함수가 인라인되더라도 이 기능은 동작합니다.
- 함수에는 매크로 함정이 없습니다.
매크로를 정적 인라인 함수로 변환하더라도 매크로를 특이한 방식으로 사용하는 극소수의 C 확장에만 영향을 줄 것입니다.
대부분의 매크로가 매개변수를 PyObject*로 캐스트하므로, 하위 호환성을 위해 함수는 컴파일러 경고를 피할 수 있도록 PyObject*뿐 아니라 모든 타입을 계속 받아들여야 합니다.
Python 3.6은 C 컴파일러가 정적 인라인 함수를 지원할 것을 요구합니다: PEP 7은 C99의 부분 집합을 요구합니다.
상태: 완료됨 (Python 3.9에서)
Python 3.8에서 정적 인라인 함수로 변환된 매크로:
Py_INCREF(),Py_DECREF()Py_XINCREF(),Py_XDECREF()PyObject_INIT(),PyObject_INIT_VAR()_PyObject_GC_TRACK(),_PyObject_GC_UNTRACK(),_Py_Dealloc()
Python 3.9에서 일반 함수로 변환된 매크로:
Py_EnterRecursiveCall(),Py_LeaveRecursiveCall()(제한된 C API에 추가됨)PyObject_INIT(),PyObject_INIT_VAR()PyObject_GET_WEAKREFS_LISTPTR()PyObject_CheckBuffer()PyIndex_Check()PyObject_IS_GC()PyObject_NEW()(PyObject_New()의 별칭),PyObject_NEW_VAR()(PyObject_NewVar()의 별칭)PyType_HasFeature()(항상PyType_GetFlags()를 호출합니다)Py_TRASHCAN_BEGIN_CONDITION()및Py_TRASHCAN_END()매크로는 이제PyThreadState구조체의 멤버에 직접 접근하는 대신 구현 세부 사항을 숨기는 함수를 호출합니다.
구조체를 불투명하게 만들기
C API의 다음 구조체가 불투명해집니다:
PyInterpreterStatePyThreadStatePyGC_HeadPyTypeObjectPyObject및PyVarObjectPyTypeObjectPyObject또는PyVarObject를 상속하는 모든 타입
C 확장은 구조체 멤버를 가져오거나 설정할 때 getter 또는 setter 함수를 사용해야 합니다. 예를 들어, tuple->ob_item[0]은 PyTuple_GET_ITEM(tuple, 0)으로 바꿔야 합니다.
참조 카운팅에서 벗어나려면 PyObject가 불투명해져야 합니다. 현재 참조 카운터 PyObject.ob_refcnt는 C API에 노출되어 있습니다. 모든 구조체는 PyObject를 “상속”하므로 모두 불투명해져야 합니다. 예를 들어, PyFloatObject는 PyObject를 상속합니다.:
typedef struct {
PyObject ob_base;
double ob_fval;
} PyFloatObject;
PyObject를 완전히 불투명하게 만들려면 Py_INCREF()와 Py_DECREF() 매크로를 함수 호출로 바꿔야 합니다. 이 변경은 성능에 영향을 줍니다. 이 변경은 구조체를 불투명하게 만드는 작업 중 가장 마지막에 이루어지는 변경 가운데 하나가 될 가능성이 높습니다.
PyTypeObject 구조체를 불투명하게 만들면 타입을 정적으로 선언하는 C 확장(예: static PyTypeObject MyType = {...};)이 동작하지 않게 됩니다. 이러한 C 확장은 대신 PyType_FromSpec()을 사용하여 타입을 힙에 할당해야 합니다. 힙 타입을 사용하면 서브인터프리터와 호환되는 등 다른 장점도 있습니다. PEP 489 “다단계 확장 모듈 초기화”와 함께 사용하면, 모듈 인스턴스를 둘 이상 만들 수 있게 되는 등 C 확장이 Python 모듈에 더 가깝게 동작합니다.
PyThreadState 구조체를 불투명하게 만들려면 C 확장이 사용하는 멤버에 대한 getter 및 setter 함수를 추가해야 합니다.
상태: 진행 중 (Python 3.8에서 시작)
PyInterpreterState 구조체는 Python 3.8에서 불투명해졌고(bpo-35886), PyGC_Head 구조체(bpo-40241)는 Python 3.9에서 불투명해졌습니다.
다음 구조체를 불투명하게 만들 수 있도록 C API를 준비하는 작업을 추적하는 이슈는 다음과 같습니다:
PyObject: bpo-39573PyTypeObject: bpo-40170PyFrameObject: bpo-40421- Python 3.9는
PyFrame_GetCode()와PyFrame_GetBack()getter 함수를 추가하고,PyFrame_GetLineNumber를 제한된 C API로 옮깁니다.
- Python 3.9는
PyThreadState: bpo-39947- Python 3.9는 getter 함수 3개를 추가합니다:
PyThreadState_GetFrame(),PyThreadState_GetID(),PyThreadState_GetInterpreter().
- Python 3.9는 getter 함수 3개를 추가합니다:
Py_TYPE()을 l-value로 사용하는 것을 금지
Py_TYPE() 함수는 객체의 타입, 즉 PyObject.ob_type 멤버를 가져옵니다. 이 함수는 타입을 설정하는 l-value로 사용할 수 있는 매크로로 구현되어 있습니다: Py_TYPE(obj) = new_type. 이런 코드는 PyObject.ob_type을 직접 수정할 수 있다는 가정에 의존합니다. 이 때문에 PyObject 구조체를 불투명하게 만들 수 없습니다.
새로운 setter 함수 Py_SET_TYPE(), Py_SET_REFCNT(), Py_SET_SIZE()가 추가되었으며, 대신 이 함수들을 사용해야 합니다.
Py_TYPE(), Py_REFCNT(), Py_SIZE() 매크로는 l-value로 사용할 수 없는 정적 인라인 함수로 바꿔야 합니다.
예를 들어, Py_TYPE() 매크로는:
#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
다음과 같이 바뀝니다:
#define _PyObject_CAST_CONST(op) ((const PyObject*)(op))
static inline PyTypeObject* _Py_TYPE(const PyObject *ob) {
return ob->ob_type;
}
#define Py_TYPE(ob) _Py_TYPE(_PyObject_CAST_CONST(ob))
STATUS: 완료됨 (Python 3.10에서)
새 함수 Py_SET_TYPE(), Py_SET_REFCNT(), Py_SET_SIZE()가 Python 3.9에 추가되었습니다.
Python 3.10에서는 Py_TYPE(), Py_REFCNT(), Py_SIZE()를 더 이상 l-value로 사용할 수 없으며, 대신 새 setter 함수를 사용해야 합니다.
새 C API 함수는 빌린 참조를 반환해서는 안 됩니다
함수가 빌린 참조를 반환하면, Python은 호출자가 언제 이 참조의 사용을 멈추는지 추적할 수 없습니다.
예를 들어, Python list 타입이 작은 정수에 특화되어 Python 객체 대신 “원시(raw)” 숫자를 직접 저장한다면, PyList_GetItem()은 임시 Python 객체를 만들어야 합니다. 문제는 이 임시 객체를 언제 삭제해야 안전한지 결정하는 것입니다.
일반적인 지침은 새 C API 함수에서 빌린 참조를 반환하지 않도록 하는 것입니다.
이 PEP는 빌린 참조를 반환하는 어떤 함수도 제거할 계획이 없습니다.
STATUS: 완료됨 (Python 3.9에서)
Python 3.9에서 Python 객체를 반환하는 새 C API 함수는 강한 참조만 반환합니다:
PyFrame_GetBack()PyFrame_GetCode()PyObject_CallNoArgs()PyObject_CallOneArg()PyThreadState_GetFrame()
PyObject**를 반환하는 함수 피하기
PySequence_Fast_ITEMS() 함수는 PyObject* 객체 배열에 대한 직접 접근을 제공합니다. 이 함수는 폐지 예정(deprecated)이며, 대신 PyTuple_GetItem()과 PyList_GetItem()을 사용해야 합니다.
PyTuple_GET_ITEM()을 남용하여 PyTupleObject.ob_item 멤버에 직접 접근할 수 있습니다:
PyObject **items = &PyTuple_GET_ITEM(0);
이를 막기 위해 PyTuple_GET_ITEM()과 PyList_GET_ITEM() 매크로를 정적 인라인 함수로 변환합니다.
STATUS: 시작 전
새 pythoncapi_compat.h 헤더 파일
구조체를 불투명하게 만들려면 C 확장이 getter 및 setter 함수를 사용하도록 수정해야 합니다. 실질적인 문제는 이러한 함수가 없는 이전 Python 버전에 대한 지원을 어떻게 유지하느냐입니다.
예를 들어, Python 3.10에서는 더 이상 Py_TYPE()을 l-value로 사용할 수 없습니다. 대신 새 Py_SET_TYPE() 함수를 사용해야 합니다:
#if PY_VERSION_HEX >= 0x030900A4
Py_SET_TYPE(&MyType, &PyType_Type);
#else
Py_TYPE(&MyType) = &PyType_Type;
#endif
이 코드는 Python 코드 베이스를 Python 2에서 Python 3으로 이식한 개발자들에게 익숙하게 느껴질 수 있습니다.
Python은 이전 Python 버전에 새 C API 함수를 제공하는 새 pythoncapi_compat.h 헤더 파일을 배포할 것입니다. 예시:
#if PY_VERSION_HEX < 0x030900A4
static inline void
_Py_SET_TYPE(PyObject *ob, PyTypeObject *type)
{
ob->ob_type = type;
}
#define Py_SET_TYPE(ob, type) _Py_SET_TYPE((PyObject*)(ob), type)
#endif // PY_VERSION_HEX < 0x030900A4
이 헤더 파일을 사용하면 오래된 Python 버전에서도 Py_SET_TYPE()를 사용할 수 있습니다.
개발자는 이 파일을 자신의 프로젝트에 복사할 수 있고, C 확장에 필요한 몇몇 함수만 복사하여 붙여 넣을 수도 있습니다.
상태: 진행 중(구현되었지만 아직 CPython에서 배포하지 않음)
pythoncapi_compat.h 헤더 파일은 현재 다음 위치에서 개발되고 있습니다: https://github.com/pythoncapi/pythoncapi_compat
망가지는 C 확장의 수를 줄이기 위한 절차
이 PEP에 나열된 C API 비호환 변경을 도입할 때 망가지는 C 확장의 수를 줄이기 위한 절차는 다음과 같습니다:
- 비호환 변경의 영향을 받는 인기 있는 C 확장이 얼마나 되는지 추정합니다.
- 망가지는 C 확장의 유지 관리자와 협력하여 향후의 비호환 변경에 대비해 코드를 준비하도록 합니다.
- Python에 비호환 변경을 도입합니다. 문서에서는 기존 코드를 이식하는 방법을 설명해야 합니다. 테스트할 시간을 더 확보할 수 있도록 이러한 변경은 개발 주기 초반에 병합하는 것을 권장합니다.
- 많은 수의 C 확장을 망가뜨릴 가능성이 가장 높은 변경은 capi-sig 메일링 리스트에 공지하여, C 확장 유지 관리자가 다음 Python에 대비해 프로젝트를 준비하도록 알려야 합니다.
- 변경이 너무 많은 프로젝트를 망가뜨린다면 망가지는 패키지의 수, Python 커뮤니티에서 그 패키지들이 차지하는 중요도, 변경 자체의 중요도를 고려하여 변경을 되돌리는 것을 논의해야 합니다.
여기서 협력이란 보통 프로젝트에 이슈를 보고하는 것을 의미하며, 변경을 제안하는 것까지 포함하기도 합니다. 망가지는 모든 프로젝트의 수정 사항이 포함된 새 릴리스가 나올 때까지 기다려야 하는 것은 아닙니다.
C API를 직접 사용하기보다 Cython을 사용하여 작성되는 C 확장이 점점 늘어나고 있으므로, Cython이 비호환 변경에 미리 대비하도록 하는 것이 중요합니다. 그러면 C 확장 유지 관리자가 업데이트된 Cython으로 생성한 코드로 새 버전을 릴리스할 시간을 더 확보할 수 있습니다(Cython이 생성한 코드를 배포하는 C 확장의 경우).
향후의 비호환 변경은 문서에서 함수를 폐지 예정(deprecated)으로 표시하고 함수에 Py_DEPRECATED()를 달아서 예고할 수 있습니다. 하지만 구조체를 불투명하게 만드는 것과 매크로를 l-value로 사용하지 못하게 하는 것은 Py_DEPRECATED()로 폐지 예정을 표시할 수 없습니다.
중요한 것은 협력, 그리고 CPython의 발전과 하위 호환성 사이에서 균형을 찾는 것입니다. 예를 들어, PyPI에 있는 아무 오래되고 잘 알려지지 않았으며 유지 관리되지 않는 C 확장을 망가뜨리는 것은 numpy를 망가뜨리는 것보다 덜 심각합니다.
변경을 되돌린 경우에는 변경을 더 잘 준비하기 위해 협력 단계로 돌아갑니다. 더 많은 C 확장이 준비되면 비호환 변경을 다시 검토할 수 있습니다.
버전 이력
- 버전 3, 2020년 6월: PEP를 처음부터 다시 작성했습니다. 이제 Python은 새로운
pythoncapi_compat.h헤더를 배포하며, 이 PEP에 나열된 C API 비호환 변경을 도입할 때 망가지는 C 확장의 수를 줄이기 위한 절차가 정의되었습니다. - 버전 2, 2020년 4월: PEP: Modify the C API to hide implementation details.
- 버전 1, 2017년 7월: python-ideas에 보낸 PEP: Hide implementation details in the C API
Copyright
This document has been placed in the public domain.