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

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

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

거부 통지

이 PEP는 호출 가능 객체를 위한 더 단순한 공개 C API를 제안하는 PEP 590을 지지하여 거부되었습니다.

개요

새로운 “C 호출” 프로토콜을 제안합니다. 빠른 호출을 구현해야 하는 함수 또는 메서드를 나타내는 클래스에 사용하기 위한 것입니다. 목표는 내장 함수에 대한 기존의 모든 최적화를 임의의 확장 타입으로 일반화하는 것입니다.

참조 구현에서는 이 새로운 프로토콜을 기존 클래스 builtin_function_or_methodmethod_descriptor 에 사용합니다. 그러나 앞으로는 더 많은 클래스가 이를 구현할 수 있습니다.

NOTE: 이 PEP는 Python/C API만 다루며, Python 언어나 표준 라이브러리에는 영향을 주지 않습니다.

동기

표준 함수/메서드 클래스 builtin_function_or_methodmethod_descriptor 를 사용하면 C 코드를 매우 효율적으로 호출할 수 있습니다. 그러나 이들은 서브클래스화할 수 없으므로 많은 애플리케이션에 적합하지 않습니다. 예를 들어, 인트로스펙션 지원이 제한적입니다(시그니처에는 __text_signature__만 사용할 수 있고, 임의의 __qualname__은 사용할 수 없으며, inspect.getfile()도 사용할 수 없습니다). 또한 functools.partial 또는 functools.lru_cache와 같은 것을 구현하기 위한 추가 데이터를 저장할 수도 없습니다. 따라서 사용자가 C에서 사용자 지정 함수/메서드 클래스를 구현하려는 데에는 여러 이유가 있습니다(덕 타이핑 관점에서). 안타깝게도 이러한 사용자 지정 클래스는 표준 CPython 함수 클래스보다 필연적으로 느립니다. 바이트코드 인터프리터에는 builtin_function_or_method, method_descriptor, methodfunction 의 인스턴스에 특화된 여러 최적화가 있기 때문입니다.

이 PEP는 기존 코드도 단순화할 수 있게 합니다. builtin_function_or_methodmethod_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_dictoffsettp_weaklistoffset과 유사합니다). 이 값은 반드시 엄격히 양의 정수여야 합니다. 해당 오프셋에 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에 음수를 허용하여 끝에서부터 계산하는 의미로 사용하지 않습니다. 이에 대한 사용 사례는 없어 보이며, 구현만 복잡하게 만들기 때문입니다.

부모

__parent__ 또는 __objclass__ 디스크립터를 Python 코드에서 사용하는 경우와 같이 접근할 수 있는 cc_parent 필드는 임의의 Python 객체 또는 NULL일 수 있습니다. 사용자 정의 클래스는 원하는 대로 cc_parent를 자유롭게 설정할 수 있습니다. CCALL_OBJCLASS 플래그가 설정된 경우에만 C 호출 프로토콜에서 사용됩니다.

확장 타입의 메서드에서는 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_printtp_ccalloffset로 교체할 것을 제안합니다. Py_TPFLAGS_HAVE_CCALLPy_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의 정확한 배치는 stable ABI의 일부가 아닙니다. 따라서 tp_print 필드를 printfunc (함수 포인터)에서 Py_ssize_t로 변경하더라도 문제가 되지 않습니다. PyTypeObject 구조체의 메모리 배치가 변경되더라도 마찬가지입니다. 게다가 바이너리를 일반적으로 빌드하는 모든 시스템(Windows, Linux, macOS)에서 printfuncPy_ssize_t의 크기는 동일하므로, 바이너리 호환성 문제는 어차피 발생하지 않습니다.

C 호출 프로토콜

클래스에 Py_TPFLAGS_HAVE_CCALL 플래그가 설정되어 있으면 해당 클래스가 C 호출 프로토콜을 구현한다고 합니다. 위에서 설명했듯이, 이 경우 tp_ccalloffset > 0을 설정해야 합니다. 이러한 클래스는 이 절에서 설명하는 방식으로 __call__을 구현해야 합니다. 실제로는 tp_callPyCCall_Call로 설정하는 것을 의미합니다.

cc_func 필드는 C 함수 포인터이며, 기존 PyMethodDefml_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) (kwdsNULL이거나 딕셔너리이며, 이 딕셔너리는 피호출자가 수정해서는 안 됩니다.)
  • 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) (kwnamesNULL이거나 키워드 이름으로 이루어진 비어 있지 않은 튜플입니다.)
  • 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를 사용하여 해당 경우에 대한 switch 문을 쉽게 최적화할 수 있음을 의미합니다.

__objclass__ 검사

CCALL_OBJCLASS 플래그가 설정되고 cr_self이 NULL인 경우(확장 타입의 바인딩되지 않은 메서드에서 이에 해당합니다), 타입 검사가 수행됩니다. 함수는 하나 이상의 위치 인자를 사용하여 호출되어야 하며, 첫 번째 인자(일반적으로 self라고 부릅니다)는 cc_parent(클래스여야 합니다)의 인스턴스여야 합니다. 그렇지 않으면 TypeError가 발생합니다.

self 슬라이싱

cr_self가 NULL이 아니거나 cc_flagsCCALL_SELFARG 플래그가 설정되어 있지 않으면, self로 전달되는 인자는 단순히 cr_self입니다.

cr_self가 NULL이고 CCALL_SELFARG 플래그가 설정되어 있으면, 첫 번째 위치 인자가 args에서 제거되고 대신 C 함수에 self 인자로 전달됩니다. 사실상 첫 번째 위치 인자가 __self__로 처리됩니다. 위치 인자가 없으면 TypeError가 발생합니다.

이 과정을 “self 슬라이싱”이라고 하며, cr_self가 NULL이고 CCALL_SELFARG가 설정된 경우 함수에 self 슬라이싱이 있다고 합니다.

self 슬라이싱을 사용하는 CCALL_NOARGS 함수는 사실상 self라는 하나의 인자를 가집니다. 마찬가지로 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 호출 프로토콜에서는 함수에 __name__속성이 있어야 하며, 그 형식은 하위 클래스가 아닌 str이어야 합니다.

또한 __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 배열에 args[nargs]부터 저장됩니다.

PyCCallRootPyCCallDef 구조체에 액세스하는 매크로:

  • 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_returnc_exceptionbuiltin_function_or_method또는 method_descriptor의 실제 인스턴스를 호출할 때만 생성됩니다. 이는 단순성을 위해서이며 하위 호환성을 위해서이기도 합니다(프로파일 함수가 인식하지 못하는 객체를 받지 않도록 하기 위함입니다). 향후 PEP에서는 C 호출 프로토콜을 구현하는 임의의 클래스까지 C 수준 프로파일링을 확장할 수 있습니다.

내장 함수 및 메서드 변경

이 PEP의 참조 구현은 기존 클래스인 builtin_function_or_methodmethod_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 함수

다음 함수가 추가됩니다(stable ABI에도 추가됩니다).

  • PyObject * PyCFunction_ClsNew(PyTypeObject *cls, PyMethodDef *ml, PyObject *self, PyObject *module, PyObject *parent): 객체 구조가 PyCFunctionObject이고 클래스가 cls인 새 객체를 생성합니다. PyMethodDef 구조체의 항목은 새 객체를 구성하는 데 사용되지만, PyMethodDef 구조체에 대한 포인터는 저장되지 않습니다. C 호출 프로토콜의 플래그는 ml->ml_flags, selfparent를 기준으로 자동으로 결정됩니다.

기존 함수 PyCFunction_New, PyCFunction_NewExPyDescr_NewMethodPyCFunction_ClsNew을 사용하여 구현됩니다.

문서화되지 않은 함수 PyCFunction_GetFlagsPyCFunction_GET_FLAGS은 더 이상 사용되지 않습니다. 이 함수들은 원래의 METH_... 플래그를 cc_flags 내부의 비트 필드에 저장하는 방식으로 여전히 인위적으로 지원됩니다. PyCFunction_GetFlags가 기술적으로 stable ABI의 일부라는 사실에도 불구하고, 그런 방식으로 사용될 가능성은 매우 낮습니다. 무엇보다도 이 함수는 문서화조차 되어 있지 않습니다. 둘째, METH_FASTCALL 플래그는 안정 ABI의 일부가 아니지만 매우 널리 사용됩니다(Argument Clinic 때문입니다). 따라서 METH_FASTCALL을 지원할 수 없다면 PyCFunction_GetFlags의 사용 사례를 상상하기 어렵습니다. 또한 PyCFunction_GET_FLAGSPyCFunction_GetFlagsObjects/call.c외부의 CPython에서 전혀 사용되지 않는다는 사실은 이 함수들이 특별히 유용하지 않다는 점을 더욱 보여 줍니다.

상속

확장 타입은 기본 클래스와 동일한 방식으로 tp_calltp_descr_get을 구현하는 경우, 기본 클래스에서 타입 플래그 Py_TPFLAGS_HAVE_CCALL 및 값 tp_ccalloffset을 상속합니다. 힙 타입은 C 호출 프로토콜을 절대 상속하지 않습니다. 힙 타입은 동적으로 변경될 수 있으므로 안전하지 않기 때문입니다.

성능

이 PEP는 기존 코드의 성능에 긍정적이든 부정적이든 영향을 주지 않아야 합니다. 이는 기존 코드를 더 빠르게 만들기 위한 것이 아니라, 효율적인 새 코드를 작성할 수 있도록 하기 위한 것입니다.

다음은 성능 개선이 논의되는 python-dev 메일링 리스트의 몇 가지 참고 자료입니다:

안정 ABI

함수 PyCFunction_ClsNewstable ABI에 추가됩니다.

C 호출 프로토콜과 관련된 함수, 구조체 또는 상수는 어느 것도 안정 ABI에 추가되지 않습니다.

여기에는 두 가지 이유가 있습니다. 무엇보다도 C 호출 프로토콜의 가장 유용한 기능은 아마도 METH_FASTCALL 호출 규약입니다. 이것은 공개 API의 일부조차 아니므로( PEP 579의 issue 6도 참조하십시오), C 호출 프로토콜에서 다른 항목을 안정 ABI에 추가하는 것은 이상할 것입니다.

둘째로, 앞으로 C 호출 프로토콜을 확장할 수 있기를 원합니다. 안정 ABI에 아무것도 추가하지 않으면, 제한 없이 이를 수행할 수 있습니다.

하위 호환성

Python 인터페이스와 문서화된 C API에는 전혀 차이가 없습니다(모든 함수가 동일한 기능으로 계속 지원된다는 의미입니다).

발생할 수 있는 유일한 호환성 문제는 PyCFunctionObjectPyMethodDescrObject의 내부에 액세스하는 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.appendMETH_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 *에 접근할 수 있습니다. 여기에는 다양한 사용 사례가 있습니다.

  1. 피호출자는 cc_parent 필드를 사용할 수 있으며, 이는 PEP 573에 유용합니다.
  2. 애플리케이션은 PyCCallDef 구조체를 사용자 정의 필드로 자유롭게 확장할 수 있으며, 이후 이러한 필드에 유사한 방식으로 접근할 수 있습니다.
  3. PyCCallDef 구조체가 객체 구조체의 일부인 경우(PyCFunctionObject의 경우가 이에 해당함), 적절한 오프셋을 PyCCallDef 포인터에서 빼서 해당 PyCCallDef를 정의하는 호출 가능 객체를 가리키는 포인터를 얻을 수 있습니다.

이 PEP의 이전 버전에서는 CCALL_DEFARG 대신 호출 가능 객체를 피호출자에게 전달하는 CCALL_FUNCARG 플래그를 정의했습니다. 이 방식에도 유사한 사용 사례가 있었지만, 바운드 메서드에 대해서는 약간의 모호성이 있었습니다. “호출 가능 객체”는 바운드 메서드 객체여야 합니까, 아니면 메서드가 래핑한 원래 함수여야 합니까? 대신 PyCCallDef *를 전달하면 이러한 모호성이 사라집니다. 바운드 메서드는 래핑된 함수에서 PyCCallDef *를 사용하기 때문입니다.

tp_print 대체

tp_printtp_ccalloffset로 용도를 변경합니다. 이렇게 하면 외부 프로젝트가 C 호출 프로토콜을 이전 Python 버전으로 백포트하기가 더 쉬워지기 때문입니다. 특히 Cython 프로젝트가 이를 수행하는 데 관심을 보였습니다(https://mail.python.org/pipermail/python-dev/2018-June/153927.html 참조).

대안 제안

PEP 576은 이 PEP와 동일한 문제를 해결하기 위한 대안적 접근 방식입니다. Py_TPFLAGS_HAVE_CCALLPy_TPFLAGS_DEFAULT의 차이점에 대한 의견은 https://mail.python.org/pipermail/python-dev/2018-July/154238.html 을 참조하십시오.

논의

이 PEP가 논의된 python-dev 메일링 리스트 스레드 링크입니다.

참조 구현

참조 구현은 https://github.com/jdemeyer/cpython/tree/pep580 에서 찾을 수 있습니다.

C 호출 프로토콜을 사용하는 예시로, 다음 브랜치는 PEP 580을 사용하여 functools.lru_cache를 구현합니다: https://github.com/jdemeyer/cpython/tree/lru580

참고 자료