PEP 580 – C 호출 프로토콜
- Author:
- Jeroen Demeyer <J.Demeyer at UGent.be>
- BDFL-Delegate:
- Petr Viktorin
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 14-Jun-2018
- Python-Version:
- 3.8
- Post-History:
- 20-Jun-2018, 22-Jun-2018, 16-Jul-2018
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 공지
이 PEP는 호출 가능 객체를 위한 더 단순한 공개 C API를 제안하는 PEP 590이 채택되면서 거부되었습니다.
초록
새로운 “C 호출” 프로토콜을 제안합니다. 이 프로토콜은 빠른 호출을 구현해야 하는, 함수나 메서드를 나타내는 클래스를 위한 것입니다. 목표는 내장 함수에 대한 기존의 모든 최적화를 임의의 확장 타입으로 일반화하는 것입니다.
참조 구현에서는 이 새로운 프로토콜을 기존 클래스인 builtin_function_or_method와 method_descriptor에 사용합니다. 하지만 앞으로는 더 많은 클래스가 이 프로토콜을 구현할 수 있습니다.
참고: 이 PEP는 Python/C API만 다루며, Python 언어나 표준 라이브러리에는 영향을 주지 않습니다.
동기
표준 함수/메서드 클래스인 builtin_function_or_method와 method_descriptor를 사용하면 C 코드를 매우 효율적으로 호출할 수 있습니다. 하지만 이 클래스들은 서브클래스를 만들 수 없어서 많은 응용에 적합하지 않습니다. 예를 들어 인트로스펙션 지원이 제한적입니다(시그니처는 __text_signature__로만 제공되고, 임의의 __qualname__을 지정할 수 없으며, inspect.getfile()도 사용할 수 없습니다). 또한 functools.partial이나 functools.lru_cache 같은 것을 구현하기 위해 추가 데이터를 저장할 수도 없습니다. 따라서 사용자가 (덕 타이핑 관점에서의) 사용자 정의 함수/메서드 클래스를 C로 구현하고 싶어 할 이유는 많습니다. 안타깝게도 이러한 사용자 정의 클래스는 표준 CPython 함수 클래스보다 느릴 수밖에 없습니다. 바이트코드 인터프리터에는 builtin_function_or_method, method_descriptor, method, function의 인스턴스에만 적용되는 다양한 최적화가 있기 때문입니다.
이 PEP를 사용하면 기존 코드도 단순화할 수 있습니다. builtin_function_or_method와 method_descriptor를 검사하는 부분을 C 호출 프로토콜을 검사하고 사용하는 방식으로 간단히 대체할 수 있습니다. 향후 PEP에서 더 많은 클래스에 C 호출 프로토콜을 구현하면 코드를 더욱 단순화할 수 있습니다.
또한 C 호출 프로토콜은 앞으로 새로운 기능을 추가해 쉽게 확장할 수 있도록 설계합니다.
더 자세한 배경과 동기는 PEP 579를 참조하십시오.
개요
현재 CPython에는 몇몇 특정 함수 클래스를 빠르게 호출하기 위한 여러 최적화가 있습니다. 좋은 예로 옵코드 CALL_FUNCTION의 구현을 들 수 있으며, 그 구조는 다음과 같습니다(실제 코드 보기):
if (PyCFunction_Check(func)) {
return _PyCFunction_FastCallKeywords(func, stack, nargs, kwnames);
}
else if (Py_TYPE(func) == &PyMethodDescr_Type) {
return _PyMethodDescr_FastCallKeywords(func, stack, nargs, kwnames);
}
else {
if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
/* ... */
}
if (PyFunction_Check(func)) {
return _PyFunction_FastCallKeywords(func, stack, nargs, kwnames);
}
else {
return _PyObject_FastCallKeywords(func, stack, nargs, kwnames);
}
}
이렇게 특별하게 처리되는 클래스의 인스턴스를 tp_call 슬롯으로 호출하면 최적화를 사용할 때보다 느립니다. 이 PEP의 기본 아이디어는 사용자 C 코드에서도 호출자와 피호출자 양쪽 모두로서 그러한 최적화를 사용할 수 있게 하는 것입니다.
기존 클래스인 builtin_function_or_method와 다른 몇몇 클래스는 바탕이 되는 C 함수와 그 시그니처를 기술하기 위해 PyMethodDef 구조체를 사용합니다. 첫 번째 구체적인 변경 사항은 이것을 새로운 구조체인 PyCCallDef로 대체하는 것입니다. 이 구조체는 PyMethodDef와 같은 정보 일부를 저장하지만, 한 가지 중요한 정보가 추가됩니다. 바로 함수의 “부모”(함수가 정의된 클래스나 모듈)입니다. PyMethodDef 배열은 여전히 함수/메서드를 생성하는 데 사용되지만, 더 이상 호출하는 데에는 사용되지 않는다는 점에 유의하십시오.
두 번째로, 모든 클래스가 호출을 최적화하는 데 이러한 PyCCallDef를 사용할 수 있게 하려고 합니다. 그래서 PyTypeObject 구조체에 객체 구조체 안의 PyCCallDef *에 대한 오프셋을 제공하는 tp_ccalloffset 필드와 tp_ccalloffset이 유효함을 나타내는 Py_TPFLAGS_HAVE_CCALL 플래그가 추가됩니다.
세 번째로, (일반 함수뿐 아니라) 언바운드 메서드와 바운드 메서드도 효율적으로 처리하려고 하므로 프로토콜에서 __self__를 다루어야 합니다. 객체 구조체에서 PyCCallDef * 다음에 PyObject *self 필드가 옵니다. 이 두 필드를 합쳐서 PyCCallRoot 구조체라고 부릅니다.
이러한 새 구조체를 사용하여 객체를 효율적으로 호출하는 새 프로토콜을 “C 호출 프로토콜”이라고 부릅니다.
참고: 이 PEP에서 “언바운드 메서드”와 “바운드 메서드”라는 표현은 특정 클래스가 아니라 일반적인 동작을 가리킵니다. 예를 들어, 언바운드 메서드는 __get__을 적용하면 바운드 메서드로 바뀝니다.
새로운 자료 구조
PyTypeObject 구조체에 새 필드 Py_ssize_t tp_ccalloffset과 새 플래그 Py_TPFLAGS_HAVE_CCALL이 추가됩니다. 이 플래그가 설정되어 있으면 tp_ccalloffset은 객체 구조체 내부의 유효한 오프셋으로 간주됩니다(tp_dictoffset 및 tp_weaklistoffset과 유사합니다). 이 값은 반드시 0보다 큰 정수여야 합니다. 해당 오프셋에는 다음과 같은 PyCCallRoot 구조체가 위치합니다:
typedef struct {
const PyCCallDef *cr_ccall;
PyObject *cr_self; /* __self__ argument for methods */
} PyCCallRoot;
PyCCallDef 구조체는 함수를 어떻게 호출할 수 있는지 기술하는 데 필요한 모든 것을 담고 있습니다:
typedef struct {
uint32_t cc_flags;
PyCFunc cc_func; /* C function to call */
PyObject *cc_parent; /* class or module */
} PyCCallDef;
__self__를 PyCCallDef 바깥에 두는 이유는 PyCCallDef가 함수를 생성한 후에는 변경되지 않도록 설계되었기 때문입니다. 하나의 PyCCallDef를 언바운드 메서드 하나와 여러 바운드 메서드가 공유할 수 있습니다. __self__를 그 구조체 안에 넣는다면 이렇게 할 수 없습니다.
참고: tp_dictoffset과 달리, tp_ccalloffset에는 끝에서부터 센다는 의미의 음수를 허용하지 않습니다. 이에 대한 사용 사례가 없어 보이며, 구현을 복잡하게 만들 뿐입니다.
부모
cc_parent 필드(예를 들어 Python 코드에서 __parent__ 또는 __objclass__ 디스크립터로 접근합니다)는 임의의 Python 객체이거나 NULL일 수 있습니다. 사용자 정의 클래스는 cc_parent를 원하는 어떤 값으로든 자유롭게 설정할 수 있습니다. C 호출 프로토콜은 CCALL_OBJCLASS 플래그가 설정된 경우에만 이 필드를 사용합니다.
확장 타입의 메서드에서 cc_parent는 그 메서드를 정의한 클래스(type(self)의 슈퍼클래스일 수도 있습니다)를 가리킵니다. 현재는 메서드 코드에서 이를 얻어 내기가 간단하지 않습니다. 앞으로는 이를 이용해 정의한 클래스를 통해 모듈 상태에 접근할 수 있습니다. 자세한 내용은 PEP 573의 근거를 참조하십시오.
CCALL_OBJCLASS 플래그가 설정되어 있으면(확장 타입의 메서드에서는 설정됩니다), cc_parent는 다음과 같은 타입 검사에 사용됩니다:
>>> list.append({}, "x")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: descriptor 'append' requires a 'list' object but received a 'dict'
모듈의 함수에서는 cc_parent가 해당 모듈로 설정됩니다. 현재 이는 __self__와 완전히 같습니다. 하지만 모듈을 가리키는 데 __self__를 사용하는 것은 현재 구현의 특이한 점일 뿐입니다. 앞으로는 메서드 구현을 위해 __self__를 일반적인 방식으로 사용하는 함수를 허용하고자 합니다. 그러한 함수도 모듈을 가리킬 때는 여전히 cc_parent를 대신 사용할 수 있습니다.
부모는 일반적으로 __qualname__을 구현하는 데에도 사용됩니다. 새 C API 함수 PyCCall_GenericGetQualname()이 바로 그 일을 합니다.
tp_print 사용하기
기존에 사용되지 않는 필드인 tp_print를 tp_ccalloffset으로 교체할 것을 제안합니다. Py_TPFLAGS_HAVE_CCALL은 Py_TPFLAGS_DEFAULT에 추가되지 않으므로, tp_print를 설정하는 기존 확장 모듈에 대해 완전한 하위 호환성이 보장됩니다. 이는 또한 Py_TPFLAGS_HAVE_CCALL이 지정된 경우 tp_ccalloffset이 유효한 오프셋이어야 한다고 요구할 수 있음을 의미합니다. 즉, tp_ccalloffset != 0을 검사할 필요가 없습니다. 향후 Python 버전에서는 tp_print를 무조건 tp_ccalloffset으로 바꾸고, Py_TPFLAGS_HAVE_CCALL 플래그를 없앤 뒤 대신 tp_ccalloffset != 0을 검사하기로 결정할 수도 있습니다.
참고: PyTypeObject의 정확한 레이아웃은 안정 ABI의 일부가 아닙니다). 따라서 tp_print 필드를 printfunc(함수 포인터)에서 Py_ssize_t로 변경하는 것은 PyTypeObject 구조체의 메모리 레이아웃이 바뀌더라도 문제가 되지 않을 것입니다. 게다가 바이너리가 일반적으로 빌드되는 모든 시스템(Windows, Linux, macOS)에서 printfunc와 Py_ssize_t의 크기가 같으므로, 어차피 바이너리 호환성 문제는 발생하지 않습니다.
C 호출 프로토콜
클래스에 Py_TPFLAGS_HAVE_CCALL 플래그가 설정되어 있으면 그 클래스가 C 호출 프로토콜을 구현한다고 말합니다(위에서 설명했듯이, 이 경우 tp_ccalloffset > 0으로 설정해야 합니다). 이러한 클래스는 이 절에서 설명하는 대로 __call__을 구현해야 합니다(실제로는 tp_call을 PyCCall_Call로 설정하기만 하면 됩니다).
cc_func 필드는 C 함수 포인터이며, PyMethodDef의 기존 ml_meth 필드와 같은 역할을 합니다. 정확한 시그니처는 플래그에 따라 달라집니다. cc_func의 시그니처에 영향을 주는 플래그의 부분 집합은 비트마스크 CCALL_SIGNATURE로 주어집니다. 아래는 cc_flags & CCALL_SIGNATURE에 가능한 값과 각 경우에 C 함수가 받는 인자입니다. 반환값은 항상 PyObject *입니다. 다음은 기존 PyMethodDef 시그니처 플래그와 유사합니다:
CCALL_VARARGS:cc_func(PyObject *self, PyObject *args)CCALL_VARARGS | CCALL_KEYWORDS:cc_func(PyObject *self, PyObject *args, PyObject *kwds)(kwds는NULL이거나 딕셔너리이며, 이 딕셔너리는 피호출자가 수정해서는 안 됩니다)CCALL_FASTCALL:cc_func(PyObject *self, PyObject *const *args, Py_ssize_t nargs)CCALL_FASTCALL | CCALL_KEYWORDS:cc_func(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)(kwnames는NULL이거나 키워드 이름으로 이루어진 비어 있지 않은 튜플입니다)CCALL_NOARGS:cc_func(PyObject *self, PyObject *unused)(두 번째 인자는 항상NULL입니다)CCALL_O:cc_func(PyObject *self, PyObject *arg)
CCALL_DEFARG 플래그는 이들 중 어느 것과도 조합할 수 있습니다. 이 경우 C 함수는 self 앞의 첫 번째 인자로 추가 인자 하나를 받는데, 이는 이 호출에 사용되는 PyCCallDef 구조체에 대한 const 포인터입니다. 예를 들어, 다음과 같은 시그니처가 됩니다:
CCALL_DEFARG | CCALL_VARARGS:cc_func(const PyCCallDef *def, PyObject *self, PyObject *args)
한 가지 예외는 CCALL_DEFARG | CCALL_NOARGS입니다. 이 경우 unused 인자가 제거되므로 시그니처는 다음과 같이 됩니다
CCALL_DEFARG | CCALL_NOARGS:cc_func(const PyCCallDef *def, PyObject *self)
참고: 기존 METH_... 플래그와 달리, CCALL_... 상수가 반드시 단일 비트를 나타내는 것은 아닙니다. 따라서 if (cc_flags & CCALL_VARARGS)로 검사하는 것은 시그니처를 확인하는 올바른 방법이 아닙니다. 또한 Python 버전 간에 이 플래그들의 바이너리 호환성은 보장되지 않습니다. 이를 통해 구현은 플래그에 가장 효율적인 수치 값을 선택할 수 있습니다. 참조 구현에서 cc_flags & CCALL_SIGNATURE에 허용되는 값은 정확히 구간 [0, …, 11]을 이룹니다. 이는 컴파일러가 계산된 goto(computed goto)를 사용하여 해당 경우들에 대한 switch 문을 쉽게 최적화할 수 있음을 의미합니다.
__objclass__ 검사
CCALL_OBJCLASS 플래그가 설정되어 있고 cr_self가 NULL이면(확장 타입의 언바운드 메서드가 이러한 경우입니다), 타입 검사가 수행됩니다. 함수는 적어도 하나의 위치 인자와 함께 호출되어야 하며, 첫 번째 인자(일반적으로 self라고 부릅니다)는 cc_parent(클래스여야 합니다)의 인스턴스여야 합니다. 그렇지 않으면 TypeError가 발생합니다.
셀프 슬라이싱
cr_self가 NULL이 아니거나 cc_flags에 CCALL_SELFARG 플래그가 설정되어 있지 않으면, self로 전달되는 인자는 단순히 cr_self입니다.
cr_self가 NULL이고 CCALL_SELFARG 플래그가 설정되어 있으면, 첫 번째 위치 인자가 args에서 제거되고 대신 C 함수에 self 인자로 전달됩니다. 사실상 첫 번째 위치 인자가 __self__로 취급됩니다. 위치 인자가 없으면 TypeError가 발생합니다.
이 과정을 “셀프 슬라이싱(self slicing)”이라고 하며, cr_self가 NULL이고 CCALL_SELFARG가 설정되어 있으면 함수가 셀프 슬라이싱을 한다고 합니다.
셀프 슬라이싱을 하는 CCALL_NOARGS 함수는 사실상 하나의 인자, 즉 self를 가진다는 점에 유의하십시오. 마찬가지로, 셀프 슬라이싱을 하는 CCALL_O 함수는 두 개의 인자를 가집니다.
디스크립터 동작
C 호출 프로토콜을 지원하는 클래스는 디스크립터 프로토콜을 특정한 방식으로 구현해야 합니다.
이는 바운드 메서드를 효율적으로 구현하기 위해 필요합니다. 다른 코드가 __get__이 하는 일에 대해 가정할 수 있으면, 그렇지 않을 경우 불가능했을 최적화가 가능해집니다. 특히, 바운드 메서드와 언바운드 메서드가 PyCCallDef 구조체를 공유할 수 있도록 하고자 합니다. 또한 LOAD_METHOD/CALL_METHOD 최적화에서 사용되는 _PyObject_GetMethod의 올바른 구현도 필요합니다.
우선, func가 C 호출 프로토콜을 지원한다면 func.__set__과 func.__delete__는 구현되어서는 안 됩니다.
둘째, func.__get__은 다음과 같이 동작해야 합니다.
cr_self가 NULL이 아니면,__get__은func.__get__(obj, cls)(*args, **kwds)가func(*args, **kwds)와 정확히 동일하게 동작한다는 의미에서 아무 작업도 하지 않아야(no-op) 합니다.__get__을 아예 구현하지 않는 것도 허용됩니다.cr_self가 NULL이면,func.__get__(obj, cls)(*args, **kwds)(obj가 None이 아닌 경우)는func(obj, *args, **kwds)와 동등해야 합니다. 특히, 이 경우에는__get__이 반드시 구현되어야 합니다. 이는 Self slicing과는 무관합니다.obj는 C 함수에self인자로 전달될 수도 있고, 첫 번째 위치 인자가 될 수도 있습니다.cr_self가 NULL이면,func.__get__(None, cls)(*args, **kwds)는func(*args, **kwds)와 동등해야 합니다.
func.__get__(obj, cls) 객체에는 아무런 제한이 없습니다. 예를 들어, 후자가 C 호출 프로토콜을 구현할 필요는 없습니다. 여기서는 func.__get__(obj, cls).__call__이 무엇을 하는지만 명시합니다.
__self__와 __get__에 전혀 관심이 없는 클래스의 경우, 가장 쉬운 해결책은 cr_self = Py_None (또는 NULL이 아닌 다른 아무 값)을 할당하는 것입니다.
__name__ 속성
C 호출 프로토콜은 함수에 (서브클래스가 아닌) str 타입의 __name__ 속성이 있어야 한다고 요구합니다.
또한 __name__이 반환하는 객체는 어딘가에 저장되어 있어야 하며, 임시 객체여서는 안 됩니다. 이는 PyEval_GetFuncName이 __name__ 속성에 대한 빌린 참조를 사용하기 때문에 필요합니다([2] 참조).
제네릭 API 함수
이 절에서는 C 호출 프로토콜을 다루는 새로운 공개 API 함수 또는 매크로를 나열합니다.
int PyCCall_Check(PyObject *op):op가 C 호출 프로토콜을 구현하면 참을 반환합니다.
아래의 모든 함수와 매크로는 C 호출 프로토콜을 지원하는 모든 인스턴스에 적용됩니다. 다시 말해, PyCCall_Check(func)가 참이어야 합니다.
PyObject *PyCCall_Call(PyObject *func, PyObject *args, PyObject *kwds): 위치 인자args와 키워드 인자kwds로func를 호출합니다(kwds는 NULL일 수 있습니다). 이 함수는tp_call슬롯에 넣도록 만들어졌습니다.PyObject *PyCCall_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *kwds):args[0], …,args[nargs-1]로 주어지는nargs개의 위치 인자로func를 호출합니다. 매개변수kwds는 NULL(키워드 인자 없음),name:value항목을 가진 딕셔너리, 또는 키워드 이름으로 이루어진 튜플일 수 있습니다. 후자의 경우, 키워드 값은args[nargs]부터 시작하여args배열에 저장됩니다.
PyCCallRoot와 PyCCallDef 구조체에 접근하기 위한 매크로:
const PyCCallRoot *PyCCall_CCALLROOT(PyObject *func):func내부의PyCCallRoot구조체를 가리키는 포인터입니다.const PyCCallDef *PyCCall_CCALLDEF(PyObject *func):PyCCall_CCALLROOT(func)->cr_ccall의 축약형입니다.uint32_t PyCCall_FLAGS(PyObject *func):PyCCall_CCALLROOT(func)->cr_ccall->cc_flags의 축약형입니다.PyObject *PyCCall_SELF(PyOject *func):PyCCall_CCALLROOT(func)->cr_self의 축약형입니다.
tp_getset 배열에 넣도록 만들어진 제네릭 게터:
PyObject *PyCCall_GenericGetParent(PyObject *func, void *closure):cc_parent를 반환합니다.cc_parent가 NULL이면AttributeError를 발생시킵니다.PyObject *PyCCall_GenericGetQualname(PyObject *func, void *closure):__qualname__으로 사용하기에 적합한 문자열을 반환합니다. 가능하면cc_parent의__qualname__을 사용합니다. 또한__name__속성도 사용합니다.
프로파일링
프로파일링 이벤트 c_call, c_return, c_exception은 builtin_function_or_method 또는 method_descriptor의 실제 인스턴스를 호출할 때만 생성됩니다. 이는 단순성을 위한 것이며 하위 호환성을 위한 것이기도 합니다(프로파일 함수가 인식하지 못하는 객체를 받지 않도록 하기 위함입니다). 향후 PEP에서 C 수준 프로파일링을 C 호출 프로토콜을 구현하는 임의의 클래스로 확장할 수도 있습니다.
내장 함수와 메서드의 변경 사항
이 PEP의 참조 구현은 기존 클래스 builtin_function_or_method와 method_descriptor가 C 호출 프로토콜을 사용하도록 변경합니다. 사실 이 두 클래스는 거의 합쳐집니다. 구현은 매우 비슷해지지만, (주로 하위 호환성 때문에) 별개의 클래스로 남습니다. PyCCallDef 구조체는 단순히 객체 구조체의 일부로 저장됩니다. 두 클래스 모두 PyCFunctionObject를 객체 구조체로 사용합니다. 두 클래스의 새 레이아웃은 다음과 같습니다:
typedef struct {
PyObject_HEAD
PyCCallDef *m_ccall;
PyObject *m_self; /* Passed as 'self' arg to the C function */
PyCCallDef _ccalldef; /* Storage for m_ccall */
PyObject *m_name; /* __name__; str object (not NULL) */
PyObject *m_module; /* __module__; can be anything */
const char *m_doc; /* __text_signature__ and __doc__ */
PyObject *m_weakreflist; /* List of weak references */
} PyCFunctionObject;
모듈의 함수와 확장 타입의 언바운드 메서드의 경우, m_ccall은 _ccalldef 필드를 가리킵니다. 바운드 메서드의 경우, m_ccall은 언바운드 메서드의 PyCCallDef를 가리킵니다.
참고: 새 레이아웃에 따라 method_descriptor는 더 이상 PyDescr_COMMON으로 시작하지 않습니다. 이는 순전히 구현 세부 사항이며, 호환성 문제는 (있더라도) 거의 일으키지 않을 것입니다.
C API 함수
다음 함수가 추가됩니다(안정 ABI에도 추가됩니다):
PyObject * PyCFunction_ClsNew(PyTypeObject *cls, PyMethodDef *ml, PyObject *self, PyObject *module, PyObject *parent): 객체 구조체가PyCFunctionObject이고 클래스가cls인 새 객체를 생성합니다.PyMethodDef구조체의 항목들은 새 객체를 구성하는 데 사용되지만,PyMethodDef구조체에 대한 포인터는 저장되지 않습니다. C 호출 프로토콜의 플래그는ml->ml_flags,self,parent에 따라 자동으로 결정됩니다.
기존 함수 PyCFunction_New, PyCFunction_NewEx, PyDescr_NewMethod는 PyCFunction_ClsNew를 사용하여 구현됩니다.
문서화되지 않은 함수 PyCFunction_GetFlags와 PyCFunction_GET_FLAGS는 폐지 예정(deprecated)이 됩니다. 이 함수들은 원래의 METH_... 플래그를 cc_flags 내부의 비트 필드에 저장하는 방식으로 여전히 인위적으로 지원됩니다. PyCFunction_GetFlags가 엄밀히 말하면 안정 ABI의 일부이기는 하지만, 그런 식으로 사용될 가능성은 매우 낮습니다. 우선, 이 함수는 문서화조차 되어 있지 않습니다. 둘째, METH_FASTCALL 플래그는 안정 ABI에 포함되지 않지만, (인자 처리 도구인 Argument Clinic 때문에) 매우 흔하게 사용됩니다. 따라서 METH_FASTCALL을 지원할 수 없다면 PyCFunction_GetFlags의 사용 사례를 상상하기 어렵습니다. CPython이 Objects/call.c 바깥에서는 PyCFunction_GET_FLAGS와 PyCFunction_GetFlags를 전혀 사용하지 않는다는 사실은 이 함수들이 그다지 유용하지 않다는 것을 더욱 잘 보여 줍니다.
상속
확장 타입은 tp_call과 tp_descr_get을 베이스 클래스와 같은 방식으로 구현하는 경우, 베이스 클래스로부터 타입 플래그 Py_TPFLAGS_HAVE_CCALL과 값 tp_ccalloffset을 상속합니다. 힙 타입은 C 호출 프로토콜을 절대 상속하지 않습니다. 그렇게 하면 안전하지 않기 때문입니다(힙 타입은 동적으로 변경될 수 있습니다).
성능
이 PEP는 기존 코드의 성능에 (긍정적으로든 부정적으로든) 영향을 주지 않을 것입니다. 이 PEP의 목적은 효율적인 새 코드를 작성할 수 있게 하는 것이지, 기존 코드를 더 빠르게 만드는 것이 아닙니다.
다음은 성능 개선을 논의한 python-dev 메일링 리스트 글 몇 가지입니다:
안정 ABI
함수 PyCFunction_ClsNew가 안정 ABI에 추가됩니다.
C 호출 프로토콜을 다루는 함수, 구조체, 상수는 어느 것도 안정 ABI에 추가되지 않습니다.
여기에는 두 가지 이유가 있습니다. 첫째, C 호출 프로토콜에서 가장 유용한 기능은 아마도 METH_FASTCALL 호출 규약일 것입니다. 이것조차 공개 API의 일부가 아니라는 점을 고려하면(PEP 579의 이슈 6도 참고하십시오), C 호출 프로토콜의 다른 무언가를 안정 ABI에 추가하는 것은 이상한 일일 것입니다.
둘째, 우리는 앞으로 C 호출 프로토콜을 확장할 수 있기를 원합니다. 안정 ABI에 아무것도 추가하지 않으면 아무런 제약 없이 자유롭게 확장할 수 있습니다.
하위 호환성
Python 인터페이스와 문서화된 C API 모두에는 아무런 차이도 없습니다(모든 함수가 같은 기능으로 계속 지원된다는 의미에서 그렇습니다).
유일하게 문제가 생길 수 있는 경우는 PyCFunctionObject와 PyMethodDescrObject의 내부에 접근하는 C 코드입니다. 이로 인한 문제는 매우 적을 것으로 예상합니다.
근거
이것이 PEP 575보다 나은 이유는 무엇입니까?
PEP 575에 대한 주요 불만 중 하나는 기능(호출 및 인트로스펙션 프로토콜)을 클래스 계층 구조와 결합한다는 점이었습니다. 즉, 클래스가 base_function의 서브클래스인 경우에만 새 기능의 이점을 누릴 수 있었습니다. 기존 클래스는 기존 베이스 클래스나 구현 세부 사항에서 비롯된 C 객체 구조체 레이아웃상의 다른 제약이 있을 수 있으므로, 그렇게 하기 어려울 수 있습니다. 예를 들어, functools.lru_cache는 PEP 575를 있는 그대로 구현할 수 없습니다.
또한 바로 구현 세부 사항과 클래스 계층 구조 양쪽 모두에 변경이 필요했기 때문에 구현이 복잡해졌습니다.
현재 PEP에는 이러한 문제가 없습니다.
함수 포인터를 왜 인스턴스에 저장합니까?
객체를 호출하는 데 실제로 필요한 정보는 클래스가 아니라 인스턴스(PyCCallDef 구조체)에 저장됩니다. 이는 tp_call 슬롯이나 tp_fastcall 슬롯을 구현하려던 이전의 시도 [1]와 다릅니다.
주요 사용 사례는 내장 함수와 메서드입니다. 이들의 경우, 호출될 C 함수는 실제로 인스턴스에 따라 달라집니다.
현재 프로토콜에서는 모든 인스턴스에 대해 같은 C 함수가 호출되는 경우도 쉽게 지원할 수 있다는 점에 유의하십시오. 모든 인스턴스에 하나의 정적 PyCCallDef 구조체를 사용하기만 하면 됩니다.
왜 CCALL_OBJCLASS입니까?
플래그 CCALL_OBJCLASS는 self 인자의 클래스를 검사해야 하는 다양한 경우를 지원하기 위한 것으로, 그 예는 다음과 같습니다.:
>>> list.append({}, None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: append() requires a 'list' object but received a 'dict'
>>> list.__len__({})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: descriptor '__len__' requires a 'list' object but received a 'dict'
>>> float.__dict__["fromhex"](list, "0xff")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: descriptor 'fromhex' for type 'float' doesn't apply to type 'list'
참조 구현에서는 이 중 첫 번째 경우만 새 코드를 사용합니다. 나머지 예들은 이러한 종류의 검사가 여러 곳에서 나타난다는 것을 보여 주므로, 이에 대한 범용 지원을 추가하는 것이 타당합니다.
CCALL_SELFARG는 왜 필요합니까?
CCALL_SELFARG 플래그와 self 슬라이싱 개념은 메서드를 지원하는 데 필요합니다. C 함수는 자신이 언바운드 메서드로 호출되는지 바운드 메서드로 호출되는지 신경 쓰지 않아야 합니다. 두 경우 모두 self 인자가 있어야 하며, 이는 단순히 언바운드 메서드 호출의 첫 번째 위치 인자입니다.
예를 들어, list.append는 METH_O 메서드입니다. list.append([], 42)와 [].append(42) 호출은 모두 C 호출 list_append([], 42)로 변환되어야 합니다.
제안된 C 호출 프로토콜 덕분에, 언바운드 메서드와 바운드 메서드가 모두 (CCALL_SELFARG 플래그가 설정된) 하나의 PyCCallDef 구조체를 공유하는 방식으로 이를 지원할 수 있습니다.
따라서 CCALL_SELFARG에는 두 가지 장점이 있습니다. 메서드를 호출할 때 추가적인 간접 계층이 없으며, 바운드 메서드를 생성할 때 PyCCallDef 구조체를 설정할 필요가 없습니다.
또 다른 사소한 장점은 잘못된 호출 시그니처에 대한 오류 메시지를 Python 메서드와 내장 메서드 사이에서 더 일관되게 만들 수 있다는 점입니다. 다음 예에서 Python은 메서드가 인자를 1개 받는지 2개 받는지 결정하지 못합니다:
>>> class List(list):
... def myappend(self, item):
... self.append(item)
>>> List().myappend(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: myappend() takes 2 positional arguments but 3 were given
>>> List().append(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: append() takes exactly one argument (2 given)
현재 PyCFunction_Call은 사용자에게 보이는 실제 인자 수를 알 수 없습니다. 런타임에 (self 인자가 없는) 함수와 (self 인자가 있는) 바운드 메서드를 구별할 수 없기 때문입니다. CCALL_SELFARG 플래그는 이 차이를 명시적으로 만듭니다.
CCALL_DEFARG는 왜 필요합니까?
CCALL_DEFARG 플래그는 피호출자가 PyCCallDef *에 접근할 수 있게 합니다. 여기에는 다양한 사용 사례가 있습니다.
- 피호출자는
cc_parent필드를 사용할 수 있으며, 이는 PEP 573에 유용합니다. - 애플리케이션은 사용자 정의 필드로
PyCCallDef구조체를 자유롭게 확장할 수 있으며, 이 필드들에도 같은 방식으로 접근할 수 있습니다. PyCCallDef구조체가 객체 구조체의 일부인 경우(예를 들어 PyCFunctionObject가 그렇습니다),PyCCallDef포인터에서 적절한 오프셋을 빼서 해당PyCCallDef를 정의하는 호출 가능 객체에 대한 포인터를 얻을 수 있습니다.
이 PEP의 이전 버전에서는 CCALL_DEFARG 대신 호출 가능 객체를 피호출자에게 전달하는 CCALL_FUNCARG 플래그를 정의했습니다. 이 방식에도 비슷한 사용 사례가 있었지만, 바운드 메서드에 대해서는 모호함이 있었습니다. “호출 가능 객체”가 바운드 메서드 객체여야 합니까, 아니면 메서드가 감싸고 있는 원래 함수여야 합니까? 대신 PyCCallDef *를 전달하면 바운드 메서드가 감싸진 함수의 PyCCallDef *를 사용하므로 이러한 모호함이 사라집니다.
tp_print 대체
tp_print를 tp_ccalloffset으로 용도 변경하는 이유는 외부 프로젝트가 C 호출 프로토콜을 이전 Python 버전으로 백포트하기가 더 쉬워지기 때문입니다. 특히 Cython 프로젝트가 이를 수행하는 데 관심을 보였습니다(https://mail.python.org/pipermail/python-dev/2018-June/153927.html 참고).
대안 제안
PEP 576은 이 PEP와 같은 문제를 해결하기 위한 대안적 접근 방식입니다. PEP 576과 PEP 580의 차이에 대한 의견은 https://mail.python.org/pipermail/python-dev/2018-July/154238.html 을 참고하십시오.
논의
이 PEP가 논의된 python-dev 메일링 리스트 스레드 링크는 다음과 같습니다.
- https://mail.python.org/pipermail/python-dev/2018-June/153938.html
- https://mail.python.org/pipermail/python-dev/2018-June/153984.html
- https://mail.python.org/pipermail/python-dev/2018-July/154238.html
- https://mail.python.org/pipermail/python-dev/2018-July/154470.html
- https://mail.python.org/pipermail/python-dev/2018-July/154571.html
- https://mail.python.org/pipermail/python-dev/2018-September/155166.html
- https://mail.python.org/pipermail/python-dev/2018-October/155403.html
- https://mail.python.org/pipermail/python-dev/2019-March/156853.html
- https://mail.python.org/pipermail/python-dev/2019-March/156879.html
참조 구현
참조 구현은 https://github.com/jdemeyer/cpython/tree/pep580 에서 찾을 수 있습니다.
C 호출 프로토콜을 사용하는 예로, 다음 브랜치는 PEP 580을 사용하여 functools.lru_cache를 구현합니다: https://github.com/jdemeyer/cpython/tree/lru580
참고 문헌
Copyright
This document has been placed in the public domain.