Python

확장형 정의하기: 자습서

파이썬은 C 확장 모듈 작성자가 내장 strlist 형과 마찬가지로 파이썬 코드에서 조작할 수 있는 새로운 형을 정의할 수 있도록 합니다. 모든 확장형의 코드는 패턴을 따르지만, 시작하기 전에 이해해야 할 세부 사항이 있습니다. 이 설명서는 주제에 대한 간단한 소개입니다.

기초

CPython 런타임은 모든 파이썬 객체를 PyObject* 타입의 변수로 간주하며, 이는 모든 파이썬 객체의 “기본 타입” 역할을 합니다. PyObject 구조 자체는 객체의 :term:`참조 횟수`와 객체의 “타입 객체”를 가리키는 포인터만을 포함합니다. 실제 동작이 일어나는 곳입니다. 타입 객체는 예를 들어, 속성이 객체에서 조회되거나, 메서드가 호출되거나, 다른 객체와 곱해질 때 인터프리터에 의해 어떤 (C) 함수가 호출될지 결정합니다. 이러한 C 함수들을 “타입 메서드”라고 합니다.

따라서, 새 확장형을 정의하려면, 새 형 객체를 만들어야 합니다.

이러한 종류의 내용은 예시로만 설명할 수 있으므로, 여기는 C 확장 모듈 custom 내에 :class:`!Custom`이라는 새로운 타입을 정의하는 최소하지만 완전한 모듈입니다:

참고

여기에 표시하는 것은 정적인(static) 확장형을 정의하는 전통적인 방법입니다. 대부분의 용도에 적합해야 합니다. C API는 또한 PyType_FromSpec() 함수를 사용하여 힙 할당 확장형을 정의 할 수 있습니다만, 이 자습서에서는 다루지 않습니다.

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyObject_HEAD
    /* 여기에 타입별 필드를 추가합니다. */
} CustomObject;

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = PyDoc_STR("커스텀 객체"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

static int
custom_module_exec(PyObject *m)
{
    if (PyType_Ready(&CustomType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot custom_module_slots[] = {
    {Py_mod_exec, custom_module_exec},
    // 정적 타입을 사용할 때는 이것만 사용합니다.
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom",
    .m_doc = "확장 타입을 생성하는 예제 모듈입니다.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom(void)
{
    return PyModuleDef_Init(&custom_module);
}

이제는 한 번에 배워야 할 것이 많지만, 이전 장과 비슷해 보이기를 바랍니다. 이 파일은 세 가지를 정의합니다:

  1. Custom 객체 가 포함하는 내용: 이것은 각 Custom 인스턴스에 대해 한 번 할당되는 CustomObject 구조체입니다.

  2. Custom 타입 의 동작 방식: 이것은 인터프리터가 특정 작업을 요청할 때 검사하는 플래그 및 함수 포인트를 정의하는 CustomType 구조체입니다.

  3. custom 모듈을 정의하고 실행하는 방법: 이것은 모듈을 정의하는 PyInit_custom 함수와 연관된 custom_module 구조체, 그리고 새 모듈 객체를 설정하는 custom_module_exec 함수입니다.

첫 번째 것은:

typedef struct {
    PyObject_HEAD
} CustomObject;

This is what a Custom object will contain. PyObject_HEAD is mandatory at the start of each object struct and defines a field called ob_base of type PyObject, containing a pointer to a type object and a reference count (these can be accessed using the macros Py_TYPE and Py_REFCNT respectively). The reason for the macro is to abstract away the layout and to enable additional fields in debug builds.

참고

PyObject_HEAD 매크로 뒤에는 세미콜론이 없습니다. 실수로 추가하는 것에 주의하십시오: 일부 컴파일러는 불평할 것입니다.

물론, 객체는 일반적으로 표준 PyObject_HEAD 관용구 외에 추가 데이터를 저장합니다; 예를 들어, 표준 파이썬 floats에 대한 정의는 다음과 같습니다:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

두 번째 것은 형 객체의 정의입니다.

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

참고

신경 쓰지 않는 모든 PyTypeObject 필드를 나열하지 않고 필드의 선언 순서를 신경 쓰지 않으려면, 위와 같이 C99 스타일의 지명(designated) 초기화자를 사용하는 것이 좋습니다.

object.h에 있는 PyTypeObject의 실제 정의는 위의 정의보다 더 많은 필드를 갖습니다. 나머지 필드는 C 컴파일러에 의해 0으로 채워지며, 필요하지 않으면 명시적으로 지정하지 않는 것이 일반적입니다.

한 번에 한 필드씩 따로 다루려고 합니다:

.ob_base = PyVarObject_HEAD_INIT(NULL, 0)

이 줄은 위에서 언급한 ob_base 필드를 초기화하기 위한 필수 상용구입니다.

.tp_name = "custom.Custom",

우리 형의 이름. 이것은 객체의 기본 텍스트 표현과 일부 에러 메시지에 나타납니다, 예를 들어:

>>> "" + custom.Custom()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "custom.Custom") to str

참고: name은 모듈명과 모듈 내 타입명을 모두 포함하는 점 표기법 이름입니다. 이 경우 모듈은 custom`이고 타입은 :class:!Custom`이므로, 타입명은 custom.Custom`으로 설정합니다. 실제 표기법 import 경로를 사용하는 것이 :mod:`pydocpickle 모듈과의 타입 호환성에 중요합니다.

.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,

이것은 Python이 새로운 Custom 인스턴스를 생성할 때 할당해야 할 메모리 양을 알 수 있도록 하기 위함입니다. :c:member:`~PyTypeObject.tp_itemsize`는 가변 크기 객체에만 사용되며, 그 외 경우에는 0이어야 합니다.

참고

만약 타입이 Python에서 하위 클래스화되기를 원하고, 타입이 기반 타입과 동일한 tp_basicsize 를 가지고 있다면, 다중 상속 문제에 직면할 수 있습니다. 귀하의 타입의 Python 하위 클래스는 해당 타입의 __bases__ 에 가장 먼저 나열해야 하며, 그렇지 않으면 오류를 발생시키지 않고 타입의 __new__() 메서드를 호출할 수 없습니다. 귀하의 타입이 기반 타입보다 더 큰 tp_basicsize 값을 갖도록 보장함으로써 이 문제를 피할 수 있습니다. 대부분의 경우, 기본 타입이 object 이거나 기본 타입에 데이터 멤버를 추가하여 크기를 증가시키기 때문에, 이 조건은 기본적으로 충족됩니다.

클래스 플래그를 :c:macro:`Py_TPFLAGS_DEFAULT`로 설정합니다.

.tp_flags = Py_TPFLAGS_DEFAULT,

모든 형은 이 상수를 플래그에 포함해야 합니다. 적어도 파이썬 3.3까지 정의된 모든 멤버를 활성화합니다. 추가 멤버가 필요하면, 해당 플래그를 OR 해야 합니다.

tp_doc에 형의 독스트링을 제공합니다.

.tp_doc = PyDoc_STR("Custom 객체"),

객체 생성을 활성화하려면 tp_new 핸들러를 제공해야 합니다. 이것은 Python 메서드 :meth:`~object.__new__`와 동일하지만, 명시적으로 지정해야 합니다. 이 경우, API 함수 :c:func:`PyType_GenericNew`가 제공하는 기본 구현을 사용할 수 있습니다.

.tp_new = PyType_GenericNew,

파일의 다른 모든 부분은 익숙할 것이지만, custom_module_exec()::에 있는 일부 코드는 다릅니다.

if (PyType_Ready(&CustomType) < 0) {
    return -1;
}

이것은 Custom 타입을 초기화하고, 처음에 NULL 로 설정했던 ob_type 을 포함하여 적절한 기본값으로 여러 멤버를 채웁니다.

if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
    return -1;
}

이것은 해당 타입을 모듈 딕셔너리에 추가합니다. 이를 통해 Custom 클래스를 호출하여 Custom 인스턴스를 생성할 수 있습니다.

>>> import custom
>>> mycustom = custom.Custom()

끝입니다! 남은 것은 빌드하는 것뿐입니다. 위 코드를 :file:`custom.c`라는 파일에 넣고,

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "custom"
version = "1"

그리고 :file:`pyproject.toml`에 넣은 다음,

from setuptools import Extension, setup
setup(ext_modules=[Extension("custom", ["custom.c"])])

setup.py라는 파일에 넣은 다음; 다음을

쉘에서 $ python -m pip install .

파일 custom.so`가 하위 디렉토리에 생성되어 설치되는 것을 확인할 있어야 합니다. 이제 Python을 실행해 보세요. ``import custom``을 사용할 있고 ``Custom` 객체와 가지고 놀 수 있을 것입니다.

그렇게 어렵지 않습니다, 그렇지 않나요?

물론, 현재 Custom 형은 그리 흥미롭지 않습니다. 데이터가 없고 아무것도 하지 않습니다. 서브 클래싱조차 할 수 없습니다.

기초 예제에 데이터와 메서드 추가하기

기본 예제를 확장하여 일부 데이터와 메서드를 추가해 봅시다. 또한 타입을 기본 클래스로 사용할 수 있도록 만들어 봅시다. 이러한 기능을 추가하는 새로운 모듈, :mod:`!custom2`를 생성할 것입니다:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* offsetof()를 위해 */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* 이름(first name) */
    PyObject *last;  /* 성(last name) */
    int number;
} CustomObject;

static void
Custom_dealloc(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_XSETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_XSETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"first", Py_T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", Py_T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* 센티널(Sentinel) */
};

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* 센티널(Sentinel) */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom2.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
};

static int
custom_module_exec(PyObject *m)
{
    if (PyType_Ready(&CustomType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot custom_module_slots[] = {
    {Py_mod_exec, custom_module_exec},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom2",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom2(void)
{
    return PyModuleDef_Init(&custom_module);
}

이 버전의 모듈에는 여러 가지 변경 사항이 있습니다.

Custom 타입은 이제 C 구조체에 세 가지 데이터 어트리뷰트인 first, last, 및 number*를 갖습니다. *first*와 *last 변수는 이름과 성을 포함하는 Python 문자열입니다. number 속성은 C 정수입니다.

객체 구조체는 다음과 같이 갱신됩니다:

typedef struct {
    PyObject_HEAD
    PyObject *first; /* 이름 */
    PyObject *last;  /* 성 */
    int number;
} CustomObject;

이제 관리할 데이터가 있기 때문에, 객체 할당과 할당 해제에 관해 더욱 신중해야 합니다. 최소한, 할당 해제 메서드가 필요합니다:

static void
Custom_dealloc(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(self);
}

이는 tp_dealloc 멤버에 대입됩니다:

.tp_dealloc = Custom_dealloc,

이 메서드는 두 개의 Python 속성 참조 횟수를 먼저 제거합니다. Py_XDECREF() 는 인수가 NULL 인 경우(tp_new 가 도중에 실패했을 때 발생할 수 있음)를 올바르게 처리합니다. 그런 다음 객체 타입(Py_TYPE(self) 으로 계산됨)의 tp_free 멤버를 호출하여 객체의 메모리를 해제합니다. 객체의 타입이 CustomType 이 아닐 수 있다는 점에 유의하십시오. 왜냐하면 객체가 서브클래스의 인스턴스일 수 있기 때문입니다.

참고

위의 CustomObject * 로 명시적 캐스팅이 필요한 이유는 Custom_deallocPyObject * 인수를 받도록 정의했기 때문이며, 이는 tp_dealloc 함수 포인터가 PyObject * 인수를 받을 것으로 예상하기 때문입니다. 타입의 tp_dealloc 슬롯에 할당함으로써, 이 슬롯이 오직 우리의 CustomObject 클래스의 인스턴스일 때만 호출될 수 있음을 선언하므로, (CustomObject *) 로의 캐스팅은 안전합니다. 이것이 C에서의 객체 지향 다형성입니다!

기존 코드나 이 튜토리얼의 이전 버전에서는 유사한 함수가 서브타입 객체 구조체에 대한 포인터(CustomObject*)를 직접 받는 것을 볼 수 있습니다. 예:

Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}
...
.tp_dealloc = (destructor) Custom_dealloc,

이것은 CPython이 지원하는 모든 아키텍처에서 동일한 작업을 수행하지만, C 표준에 따르면 정의되지 않은 동작을 유발합니다.

우리는 성과 이름이 빈 문자열로 초기화되도록 하고 싶어서, tp_new 구현을 제공합니다:

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

그리고 그것을 tp_new 멤버에 설치합니다:

.tp_new = Custom_new,

tp_new 핸들러는 타입을 위한 객체를 생성(초기화하는 것이 아님)하는 역할을 합니다. 이는 Python에서는 __new__() 메서드로 노출됩니다. tp_new 멤버를 정의할 필요는 없으며, 실제로 많은 확장 타입은 위 Custom 타입의 첫 번째 버전에서 수행된 것처럼 PyType_GenericNew() 를 재사용할 것입니다. 이 경우, 우리는 tp_new 핸들러를 사용하여 firstlast 속성을 비-NULL 기본값으로 초기화합니다.

tp_new는 인스턴스 화 되는 형(서브 클래스가 인스턴스 화 되면, 반드시 CustomType일 필요는 없습니다)과 형이 호출될 때 전달된 모든 인자가 전달되며, 만들어진 인스턴스를 반환할 것으로 기대됩니다. tp_new 처리기는 항상 위치와 키워드 인자를 받아들이지만, 종종 인자를 무시하고 인자 처리를 초기화 (C의 tp_init나 파이썬의 __init__) 메서드에게 남겨둡니다.

참고

인터프리터가 직접 할 것이라서, tp_new는 명시적으로 tp_init를 호출하면 안 됩니다.

tp_new 구현은 tp_alloc 슬롯을 호출하여 메모리를 할당합니다:

self = (CustomObject *) type->tp_alloc(type, 0);

메모리 할당이 실패할 수 있어서, 진행하기 전에 tp_alloc 결과가 NULL이 아닌지 확인해야 합니다.

참고

우리는 tp_alloc 슬롯을 직접 채우지 않았습니다. 대신 PyType_Ready()가 베이스 클래스(기본적으로 object입니다)에서 상속하여 이를 채웁니다. 대부분의 형은 기본 할당 전략을 사용합니다.

참고

만약 협력적인 tp_new (베이스 타입의 tp_new 또는 __new__() 를 호출하는 것)를 생성하는 경우, 런타임에 메서드 해석 순서(MRO)를 사용하여 어떤 메서드를 호출할지 결정하려고 해서는 안 됩니다. 항상 어떤 타입을 호출할지 정적으로 결정하고, 그 타입의 tp_new 를 직접 호출하거나 type->tp_base->tp_new 를 통해 호출해야 합니다. 이렇게 하지 않으면, 다른 Python 정의 클래스로부터 상속받는 유형의 Python 서브클래스가 제대로 작동하지 않을 수 있습니다. (특히, TypeError 가 발생하지 않고 그러한 서브클래스의 인스턴스를 생성할 수 없을 수도 있습니다.)

인스턴스의 초깃값을 제공하는 인자를 받아들이는 초기화 함수도 정의합니다:

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_XDECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_XDECREF(tmp);
    }
    return 0;
}

이것으로 tp_init 슬롯을 채웁니다:

.tp_init = Custom_init,

tp_init 슬롯은 Python에서는 __init__() 메서드로 노출됩니다. 이는 객체가 생성된 후 초기화하는 데 사용됩니다. 초기화기는 항상 위치 및 키워드 인수를 받으며, 성공 시 0 또는 실패 시 -1 을 반환해야 합니다.

tp_new 핸들러와 달리, tp_init``이 호출될 것이라는 보장은 없습니다 (예를 들어, :mod:`pickle` 모듈은 기본적으로 언픽클된 인스턴스에 대해 :meth:`~object.__init__`을 호출하지 않습니다). 또한 여러 호출될 수도 있습니다. 누구나 우리의 객체에 :meth:`!__init__` 메서드를 호출할 있습니다. 이러한 이유로 속성 값을 할당할 매우 조심해야 합니다. 예를 들어, 다음과 같이 ``first 멤버를 할당하고 싶은 유혹에 빠질 수 있습니다:

if (first) {
    Py_XDECREF(self->first);
    Py_INCREF(first);
    self->first = first;
}

하지만 이것은 위험합니다. 우리의 타입은 first 멤버의 타입을 제한하지 않으므로, 어떤 종류의 객체일 수도 있습니다. first 멤버에 접근하려고 시도하는 코드를 실행하는 소멸자를 가질 수 있습니다. 또는 해당 소멸자가 :term:`thread state <attached thread state>`를 분리하고 다른 스레드에서 접근하고 수정하는 임의의 코드를 실행하도록 할 수도 있습니다.

편집증적이 되고 이 가능성으로부터 우리 자신을 보호하기 위해, 우리는 거의 항상 참조 횟수를 줄이기 전에 멤버를 다시 대입합니다. 언제 이렇게 하지 않아도 될까요?

  • 참조 횟수가 1보다 크다는 것을 확실히 알고 있을 때;

  • when we know that deallocation of the object [1] will neither detach the thread state nor cause any calls back into our type’s code;

  • 순환 가비지 수거를 지원하지 않는 형의 tp_dealloc 처리기에서 참조 횟수를 감소시킬 때 [2].

인스턴스 변수를 어트리뷰트로 노출하려고 합니다. 이를 수행하는 방법에는 여러 가지가 있습니다. 가장 간단한 방법은 멤버 정의를 정의하는 것입니다:

static PyMemberDef Custom_members[] = {
    {"first", Py_T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", Py_T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

그리고 tp_members 슬롯에 정의를 넣습니다:

.tp_members = Custom_members,

각 멤버 정의에는 멤버 이름, 형, 오프셋, 액세스 플래그 및 독스트링이 있습니다. 자세한 내용은 아래 범용 어트리뷰트 관리 섹션을 참조하십시오.

이 접근법의 단점은 파이썬 어트리뷰트에 대입할 수 있는 객체의 형을 제한할 방법을 제공하지 않는다는 것입니다. 이름과 성은 문자열일 것으로 기대하지만, 모든 파이썬 객체를 할당할 수 있습니다. 또한 어트리뷰트를 삭제할 수 있습니다, C 포인터를 NULL로 설정합니다. NULL이 아닌 값으로 멤버를 초기화 할 수 있지만, 어트리뷰트를 삭제하면 멤버를 NULL로 설정할 수 있습니다.

하나의 메서드, :meth:`!Custom.name`을 정의하며, 이 메서드는 객체의 이름을 이름의 첫 번째와 마지막 이름을 연결하여 출력합니다.

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

이 메서드는 첫 번째 인자로 Custom (또는 Custom 하위 클래스) 인스턴스를 받는 C 함수로 구현됩니다. 메서드는 항상 인스턴스를 첫 번째 인자로 받습니다. 메서드는 종종 위치 인자 및 키워드 인자도 받지만, 이 경우 우리는 아무것도 받지 않으므로 위치 인자 튜플이나 키워드 인자 딕셔너리를 수락할 필요가 없습니다. 이 메서드는 다음 Python 메서드와 같습니다:

def name(self):
    return "%s %s" % (self.first, self.last)

firstlast 멤버가 NULL 일 가능성을 확인해야 합니다. 이는 이들이 삭제될 수 있으며, 이 경우 NULL 로 설정되기 때문입니다. 이러한 속성의 삭제를 방지하고 속성 값을 문자열로 제한하는 것이 더 좋습니다. 다음 섹션에서 방법을 살펴보겠습니다.

이제 메서드를 정의했습니다, 메서드 정의 배열을 만들어야 합니다:

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "이름을 반환하며, 이름의 첫 번째와 마지막 이름을 조합합니다."
    },
    {NULL}  /* Sentinel */
};

(주목할 점은 메서드가 self 를 제외하고 인자를 기대하지 않도록 METH_NOARGS 플래그를 사용했다는 것입니다)

그리고 tp_methods 슬롯에 대입합니다:

.tp_methods = Custom_methods,

마지막으로, 우리의 타입을 하위 클래스화에 사용 가능한 기본 클래스로 만들 것입니다. 지금까지 작성한 메서드는 생성되거나 사용되는 객체의 유형에 대해 어떠한 가정도 하지 않도록 주의 깊게 작성되었으므로, 우리가 해야 할 일은 클래스 플래그 정의에 :c:macro:`Py_TPFLAGS_BASETYPE`를 추가하는 것입니다:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,

PyInit_custom`을 :c:func:()!PyInit_custom2`로 이름을 바꾸고, PyModuleDef 구조체에서 모듈 이름을 업데이트하며, PyTypeObject 구조체에서 전체 클래스 이름을 업데이트합니다.

마지막으로, 새 모듈을 포함하도록 setup.py 파일을 업데이트합니다.

from setuptools import Extension, setup
setup(ext_modules=[
    Extension("custom", ["custom.c"]),
    Extension("custom2", ["custom2.c"]),
])

그리고 import custom2 를 할 수 있도록 재설치합니다:

쉘에서 $ python -m pip install .

데이터 어트리뷰트를 더 세밀하게 제어하기

이 섹션에서는 Custom 예제에서 firstlast 속성이 설정되는 방식에 대해 더 세밀하게 제어할 수 있습니다. 모듈의 이전 버전에서는 인스턴스 변수 first 및 :attr:`!last`가 문자열이 아닌 값으로 설정되거나 심지어 삭제될 수도 있었습니다. 이 속성에는 항상 문자열이 포함되도록 하고 싶습니다.

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* offsetof()를 위해 */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* 이름(first name) */
    PyObject *last;  /* 성(last name) */
    int number;
} CustomObject;

static void
Custom_dealloc(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_SETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_SETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* 센티널(Sentinel) */
};

static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    Py_SETREF(self->first, Py_NewRef(value));
    return 0;
}

static PyObject *
Custom_getlast(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->last);
}

static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_SETREF(self->last, Py_NewRef(value));
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, Custom_setlast,
          "last name", NULL},
    {NULL}  /* 센티널(Sentinel) */
};

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* 센티널(Sentinel) */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom3.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static int
custom_module_exec(PyObject *m)
{
    if (PyType_Ready(&CustomType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot custom_module_slots[] = {
    {Py_mod_exec, custom_module_exec},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom3",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom3(void)
{
    return PyModuleDef_Init(&custom_module);
}

firstlast 속성에 대한 더 큰 제어를 제공하기 위해, 사용자 정의 getter 및 setter 함수를 사용할 것입니다. 여기는 first 속성을 가져오고 설정하기 위한 함수입니다:

static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    Py_INCREF(self->first);
    return self->first;
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    PyObject *tmp;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    tmp = self->first;
    Py_INCREF(value);
    self->first = value;
    Py_DECREF(tmp);
    return 0;
}

getter 함수에는 Custom 객체와 void 포인터인 “클로저”가 전달됩니다. 이 경우 클로저는 무시됩니다. (클로저는 정의 데이터가 getter와 setter에 전달되는 고급 사용을 지원합니다. 예를 들어, 클로저의 데이터를 기반으로 가져오거나 설정할 속성을 결정하는 단일 getter 및 setter 함수 세트를 허용하는 데 사용될 수 있습니다.)

setter 함수에는 Custom 객체, 새 값, 그리고 클로저가 전달됩니다. 새 값은 NULL 일 수 있으며, 이 경우 해당 속성은 삭제되고 있습니다. 우리의 setter에서는 속성이 삭제되었거나 새 값이 문자열이 아닌 경우 오류를 발생시킵니다.

PyGetSetDef 구조체의 배열을 만듭니다:

static PyGetSetDef Custom_getsetters[] = {
    {"first", Custom_getfirst, Custom_setfirst,
     "이름", NULL},
    {"last", Custom_getlast, Custom_setlast,
     "성", NULL},
    {NULL}  /* Sentinel */
};

그리고 tp_getset 슬롯에 등록합니다:

.tp_getset = Custom_getsetters,

PyGetSetDef 구조체의 마지막 항목은 위에서 언급한 “클로저”입니다. 이 경우, 클로저를 사용하지 않아서, NULL만 전달합니다.

또한 이러한 어트리뷰트에 대한 멤버 정의를 제거합니다:

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "사용자 지정 숫자"},
    {NULL}  /* Sentinel */
};

또한 문자열만 전달되도록 [3] tp_init 처리기를 갱신해야 합니다:

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_DECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_DECREF(tmp);
    }
    return 0;
}

이러한 변경을 통해, firstlast 멤버가 절대 NULL이 아니라고 확신할 수 있어서, 거의 모든 경우에 NULL 값 검사를 제거할 수 있습니다. 이것은 대부분의 Py_XDECREF() 호출이 Py_DECREF() 호출로 변환될 수 있음을 의미합니다. 이러한 호출을 변경할 수 없는 유일한 장소는 tp_dealloc 구현에서인데, tp_new에서 이 멤버의 초기화가 실패했을 가능성이 있습니다.

또한 이전과 마찬가지로, 초기화 함수에서 모듈 초기화 함수와 모듈 이름을 바꾸고, setup.py 파일에 추가 정의를 추가합니다.

순환 가비지 수거 지원하기

파이썬에는 참조 횟수가 0이 아닐 때도 불필요한 객체를 식별할 수 있는 순환 가비지 수거기 (GC)가 있습니다. 이것은 객체가 순환에 참여할 때 일어날 수 있습니다. 예를 들어, 다음을 고려하십시오:

>>> l = []
>>> l.append(l)
>>> del l

이 예에서, 자신을 포함하는 리스트를 만듭니다. 삭제해도 여전히 자체 참조가 있습니다. 참조 횟수가 0으로 떨어지지 않습니다. 다행스럽게도, 파이썬의 순환 가비지 수거기는 결국 리스트가 가비지임을 확인하고 해제합니다.

Custom 예제의 두 번째 버전에서는 임의의 객체를 first 또는 last 속성에 저장할 수 있도록 허용했습니다 [4]. 게다가, 두 번째 및 세 번째 버전에서는 Custom`의 하위 클래스화를 허용했으며, 하위 클래스는 임의의 속성을 추가할 있습니다. 가지 이유로 인해, :class:!Custom` 객체는 순환에 참여할 수 있습니다:

>>> import custom3
>>> class Derived(custom3.Custom): pass
...
>>> n = Derived()
>>> n.some_attribute = n

Custom 인스턴스가 참조 순환에 참여할 경우 이를 적절하게 감지하고 순환 GC에 의해 수집할 수 있도록 하려면, 당사의 Custom 타입은 추가 슬롯 두 개를 채우고 이 슬롯을 활성화하는 플래그를 활성화해야 합니다:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* offsetof()를 위함 */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* 이름 (first name) */
    PyObject *last;  /* 성 (last name) */
    int number;
} CustomObject;

static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

static int
Custom_clear(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

static void
Custom_dealloc(PyObject *op)
{
    PyObject_GC_UnTrack(op);
    (void)Custom_clear(op);
    Py_TYPE(op)->tp_free(op);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_SETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_SETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* 센티넬 (Sentinel) */
};

static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    Py_XSETREF(self->first, Py_NewRef(value));
    return 0;
}

static PyObject *
Custom_getlast(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->last);
}

static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_XSETREF(self->last, Py_NewRef(value));
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, Custom_setlast,
     "last name", NULL},
    {NULL}  /* 센티넬 (Sentinel) */
};

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* 센티넬 (Sentinel) */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom4.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
    .tp_new = Custom_new,
    .tp_init = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_traverse = Custom_traverse,
    .tp_clear = Custom_clear,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static int
custom_module_exec(PyObject *m)
{
    if (PyType_Ready(&CustomType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot custom_module_slots[] = {
    {Py_mod_exec, custom_module_exec},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom4",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom4(void)
{
    return PyModuleDef_Init(&custom_module);
}

첫째, 탐색(traversal) 메서드는 순환 GC가 순환에 참여할 수 있는 서브 객체에 대해 알 수 있도록 합니다:

static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    int vret;
    if (self->first) {
        vret = visit(self->first, arg);
        if (vret != 0)
            return vret;
    }
    if (self->last) {
        vret = visit(self->last, arg);
        if (vret != 0)
            return vret;
    }
    return 0;
}

사이클에 참여할 수 있는 서브 객체 각각에 대해, 탐색 메서드에 전달되는 visit() 함수를 호출해야 합니다. visit() 함수는 서브 객체와 탐색 메서드에 전달된 추가 인자인 arg 를 인자로 받습니다. 비-0 값을 반환해야 하는 정수 값을 반환합니다:

파이썬은 visit 함수 호출을 자동화하는 Py_VISIT() 매크로를 제공합니다. Py_VISIT()를 사용하면, Custom_traverse에서 관용구 양을 최소화할 수 있습니다:

static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

참고

Py_VISIT()를 사용하려면 tp_traverse 구현에서 인자 이름을 visitarg로 정확하게 지정해야 합니다.

둘째, 순환에 참여할 수 있는 서브 객체를 지우는 메서드를 제공해야 합니다:

static int
Custom_clear(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

Py_CLEAR() 매크로 사용에 주목하십시오. 참조 횟수를 줄이면서 임의 형의 데이터 어트리뷰트를 지우는 권장되고 안전한 방법입니다. NULL로 설정하기 전에 어트리뷰트에서 Py_XDECREF()를 대신 호출했으면, 어트리뷰트의 파괴자가 어트리뷰트를 다시 읽는 코드(특히 참조 순환이 있으면 )를 다시 호출할 가능성이 있습니다.

참고

다음과 같이 작성하여 Py_CLEAR()를 에뮬레이션할 수 있습니다:

PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);

그런데도, 어트리뷰트를 삭제할 때 항상 Py_CLEAR()를 사용하기가 훨씬 쉽고 에러가 적습니다. 견고성을 희생하면서 세밀한 최적화를 시도하지 마십시오!

할당 해제기 Custom_dealloc은 어트리뷰트를 지울 때 임의의 코드를 호출할 수 있습니다. 이는 함수 내에서 순환 GC가 트리거 될 수 있음을 의미합니다. GC는 참조 횟수가 0이 아니라고 가정하기 때문에, 멤버를 지우기 전에 PyObject_GC_UnTrack()을 호출하여 GC에서 객체를 추적 해제해야 합니다. 다음은 PyObject_GC_UnTrack()Custom_clear를 사용하여 다시 구현된 할당 해제기입니다:

static void
Custom_dealloc(PyObject *op)
{
    PyObject_GC_UnTrack(op);
    (void)Custom_clear(op);
    Py_TYPE(op)->tp_free(op);
}

마지막으로, 클래스 플래그에 Py_TPFLAGS_HAVE_GC 플래그를 추가합니다:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,

거의 다 됐습니다. 사용자 정의 tp_alloc이나 tp_free 처리기를 작성했으면, 순환 가비지 수거를 위해 이를 수정해야 합니다. 대부분의 확장은 자동으로 제공된 버전을 사용합니다.

다른 형의 서브 클래싱

기존 형에서 파생된 새 확장형을 만들 수 있습니다. 확장이 필요한 PyTypeObject를 쉽게 사용할 수 있어서, 내장형에서 상속하기가 가장 쉽습니다. 확장 모듈 간에 이러한 PyTypeObject 구조체를 공유하기 어려울 수 있습니다.

이 예제에서는 내장 list 타입에서 상속받는 SubList 타입을 생성할 것입니다. 이 새로운 타입은 일반 리스트와 완벽하게 호환되지만, 내부 카운터를 증가시키는 추가 increment() 메서드를 갖게 됩니다:

>>> import sublist
>>> s = sublist.SubList(range(3))
>>> s.extend(s)
>>> print(len(s))
6
>>> print(s.increment())
1
>>> print(s.increment())
2
#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyListObject list;
    int state;
} SubListObject;

static PyObject *
SubList_increment(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    SubListObject *self = (SubListObject *) op;
    self->state++;
    return PyLong_FromLong(self->state);
}

static PyMethodDef SubList_methods[] = {
    {"increment", SubList_increment, METH_NOARGS,
     PyDoc_STR("increment state counter")},
    {NULL},
};

static int
SubList_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    SubListObject *self = (SubListObject *) op;
    if (PyList_Type.tp_init(op, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

static PyTypeObject SubListType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "sublist.SubList",
    .tp_doc = PyDoc_STR("SubList objects"),
    .tp_basicsize = sizeof(SubListObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_init = SubList_init,
    .tp_methods = SubList_methods,
};

static int
sublist_module_exec(PyObject *m)
{
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot sublist_module_slots[] = {
    {Py_mod_exec, sublist_module_exec},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef sublist_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "sublist",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = sublist_module_slots,
};

PyMODINIT_FUNC
PyInit_sublist(void)
{
    return PyModuleDef_Init(&sublist_module);
}

보시다시피, 소스 코드는 이전 섹션의 Custom 예제와 매우 유사합니다. 주요 차이점을 분석해 보겠습니다.

typedef struct {
    PyListObject list;
    int state;
} SubListObject;

파생형 객체의 주요 차이점은 베이스형의 객체 구조체가 첫 번째 값이어야 한다는 것입니다. 베이스형은 이미 구조체의 시작 부분에 PyObject_HEAD()를 포함합니다.

Python 객체가 SubList 인스턴스인 경우, 그 PyObject * 포인터는 PyListObject *SubListObject * 둘 다로 안전하게 캐스팅될 수 있습니다:

static int
SubList_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    SubListObject *self = (SubListObject *) op;
    if (PyList_Type.tp_init(op, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

위에서 베이스 타입의 __init__() 메서드를 호출하는 방법을 확인했습니다:

이 패턴은 사용자 정의 tp_newtp_dealloc 멤버를 갖는 형을 작성할 때 중요합니다. tp_new 처리기는 실제로 tp_alloc을 사용하여 객체의 메모리를 만들지 말고, 베이스 클래스가 자체 tp_new를 호출하여 처리하도록 해야 합니다.

PyTypeObject 구조체는 타입의 구체적인 기본 클래스를 지정하는 tp_base 를 지원합니다. 크로스 플랫폼 컴파일러 문제로 인해, 이 필드를 PyList_Type 에 대한 참조로 직접 채울 수 없으며, Py_mod_exec 함수에서 수행해야 합니다:

static int
sublist_module_exec(PyObject *m)
{
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
        return -1;
    }

    return 0;
}

PyType_Ready()를 호출하기 전에, 형 구조체에 tp_base 슬롯이 채워져 있어야 합니다. 기존 형을 파생할 때, PyType_GenericNew()tp_alloc 슬롯을 채울 필요는 없습니다 – 베이스형의 할당 함수가 상속됩니다.

그 후, PyType_Ready() 를 호출하고 타입 객체를 모듈에 추가하는 것은 기본적인 Custom 예제와 동일합니다:

각주

분실물 보관소