2. تعریف نوع‌های توسعه‌ای: آموزش

پایتون به نویسنده‌ی یک ماژول توسعه‌ای C اجازه می‌دهد نوع‌های جدیدی تعریف کند که بتوان آن‌ها را از طریق کد پایتون دستکاری کرد، بسیار شبیه به نوع‌های توکار str و list. کد همه‌ی نوع‌های توسعه‌ای از یک الگو پیروی می‌کند، اما جزئیاتی وجود دارند که پیش از شروع کار باید آن‌ها را درک کنید. این سند مقدمه‌ای ساده و روان بر این موضوع است.

2.1. مبانی

ران‌تایم CPython همه‌ی اشیاء پایتون را به‌صورت متغیرهایی از نوع PyObject* می‌بیند که به‌عنوان یک «نوع پایه» برای همه‌ی اشیاء پایتون عمل می‌کند. خودِ ساختار PyObject تنها شامل reference count شیء و اشاره‌گری به «شیء نوع» آن است. همین‌جاست که کار اصلی انجام می‌شود؛ شیء نوع تعیین می‌کند که مفسر در مواردی مانند جستجوی یک ویژگی روی یک شیء، فراخوانی یک متد یا ضرب آن در شیء دیگری، کدام توابع (C) را فراخوانی می‌کند. به این توابع C «متدهای نوع» گفته می‌شود.

بنابراین، اگر می‌خواهید یک نوع توسعه‌ای جدید تعریف کنید، باید یک شیء نوع جدید ایجاد کنید.

چنین چیزی را تنها می‌توان با مثال توضیح داد، بنابراین در اینجا یک ماژول حداقلی اما کامل ارائه می‌شود که نوع جدیدی به نام Custom را درون یک ماژول توسعه‌ای C به نام custom تعریف می‌کند:

توجه

آنچه در اینجا نشان می‌دهیم، روش سنتی تعریف نوع‌های توسعه‌ای ایستا است. این روش باید برای بیشتر کاربردها کافی باشد. API زبان C همچنین اجازه می‌دهد نوع‌های توسعه‌ای تخصیص‌یافته در هیپ را با استفاده از تابع PyType_FromSpec() تعریف کنید؛ تابعی که در این آموزش پوشش داده نشده است.

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyObject_HEAD
    /* Type-specific fields go here. */
} CustomObject;

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,
};

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},
    // Just use this while using static types
    {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 = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

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

این واقعاً مقدار زیادی است که باید یک‌باره هضم کنید، اما امیدواریم بخش‌هایی از آن از فصل پیشین آشنا به نظر برسند. این پرونده سه چیز را تعریف می‌کند:

  1. آنچه یک شیء Custom در بر دارد: این ساختار CustomObject است که یک‌بار برای هر نمونه‌ی Custom تخصیص داده می‌شود.

  2. نحوه‌ی رفتار نوع Custom: این ساختار CustomType است که مجموعه‌ای از پرچم‌ها و اشاره‌گرهای تابع را تعریف می‌کند؛ مفسر آن‌ها را زمانی که عملیات‌های خاصی درخواست می‌شوند بررسی می‌کند.

  3. نحوه تعریف و اجرای ماژول custom: این، تابع PyInit_custom و ساختار مرتبط custom_module برای تعریف ماژول، و تابع custom_module_exec برای راه‌اندازی یک شیء ماژول تازه است.

بخش اول این است:

typedef struct {
    PyObject_HEAD
} CustomObject;

این همان چیزی است که یک شیء Custom در بر خواهد گرفت. PyObject_HEAD در ابتدای ساختار هر شیء الزامی است و فیلدی به نام ob_base از نوع PyObject تعریف می‌کند که شامل یک اشاره‌گر به یک شیء نوع و یک شمارش ارجاع است (می‌توان به این‌ها به‌ترتیب با استفاده از ماکروهای Py_TYPE و Py_REFCNT دسترسی داشت). دلیل وجود این ماکرو، انتزاعی کردن چیدمان و امکان‌پذیر کردن فیلدهای اضافی در نسخه‌های اشکال‌زدایی است.

توجه

در بالا، پس از ماکروی PyObject_HEAD هیچ نقطه‌ویرگولی وجود ندارد. مراقب باشید که ناخواسته یکی اضافه نکنید: برخی کامپایلرها اعتراض خواهند کرد.

البته، اشیاء به‌طور کلی علاوه بر کد پیش‌ساخته‌ی استاندارد PyObject_HEAD داده‌های اضافی نیز ذخیره می‌کنند؛ برای مثال، تعریف اعشاری‌های استاندارد پایتون در ادامه آمده است:

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,
};

توجه

توصیه می‌کنیم مانند بالا از مقداردهنده‌های تعیین‌شده (designated initializers) به سبک C99 استفاده کنید تا از فهرست‌کردن تمام فیلدهای PyTypeObject که به آن‌ها اهمیت نمی‌دهید پرهیز کنید و همچنین از توجه به ترتیب اعلان فیلدها پرهیز کنید.

تعریف واقعی PyTypeObject در object.h نسبت به تعریف بالا فیلدهای بسیار بیشتری دارد. فیلدهای باقی‌مانده توسط کامپایلر C با صفر پر می‌شوند و رویه رایج این است که آن‌ها را صریحاً مشخص نکنید، مگر اینکه به آن‌ها نیاز داشته باشید.

می‌خواهیم آن را کالبدشکافی کنیم، هر بار یک فیلد:

.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

توجه داشته باشید که نام، یک نام نقطه‌گذاری‌شده است که هم نام ماژول و هم نام نوع درون ماژول را در بر می‌گیرد. ماژول در این مورد custom است و نوع Custom، بنابراین نام نوع را برابر با custom.Custom قرار می‌دهیم. استفاده از مسیر ایمپورت نقطه‌گذاری‌شده‌ی واقعی برای سازگار کردن نوع شما با ماژول‌های pydoc و pickle اهمیت دارد.

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

این کار برای آن است که پایتون بداند هنگام ایجاد نمونه‌های جدید Custom چه مقدار حافظه باید تخصیص دهد. tp_itemsize تنها برای شیءهای با اندازه‌ی متغیر استفاده می‌شود و در غیر این صورت باید صفر باشد.

توجه

اگر می‌خواهید نوع شما از پایتون قابل زیرکلاس‌گیری باشد و نوع شما همان tp_basicsize نوع پایه‌اش را داشته باشد، ممکن است با وراثت چندگانه مشکلاتی داشته باشید. یک زیرکلاس پایتونی از نوع شما باید نوع شما را در ابتدای __bases__ خود فهرست کند، وگرنه نخواهد توانست متد __new__() نوع شما را بدون دریافت خطا فراخوانی کند. می‌توانید با اطمینان از اینکه نوع شما مقدار بزرگ‌تری برای tp_basicsize نسبت به نوع پایه‌اش دارد، از این مشکل اجتناب کنید. در بیشتر موارد، این موضوع به هر حال برقرار خواهد بود، زیرا یا نوع پایه شما object خواهد بود، یا اعضای داده به نوع پایه خود اضافه خواهید کرد و بنابراین اندازه‌اش را افزایش می‌دهید.

پرچم‌های کلاس را برابر Py_TPFLAGS_DEFAULT قرار می‌دهیم.

.tp_flags = Py_TPFLAGS_DEFAULT,

همه‌ی نوع‌ها باید این ثابت را در پرچم‌های خود بگنجانند. این ثابت همه‌ی اعضای تعریف‌شده تا دست‌کم پایتون 3.3 را فعال می‌کند. اگر به اعضای بیشتری نیاز داشته باشید، باید پرچم‌های مربوطه را OR کنید.

ما یک رشته مستند برای این نوع در tp_doc ارائه می‌کنیم.

.tp_doc = PyDoc_STR("Custom objects"),

برای فعال‌سازی ایجاد شیء، باید هندلر tp_new را فراهم کنیم. این معادل متد پایتونی __new__() است، اما باید به‌طور صریح مشخص شود. در این مورد، می‌توانیم به‌سادگی از پیاده‌سازی پیش‌فرض ارائه‌شده توسط تابع API یعنی PyType_GenericNew() استفاده کنیم.

.tp_new = PyType_GenericNew,

بقیه‌ی محتوای پرونده باید برایتان آشنا باشد، به‌جز بخشی از کد در custom_module_exec():

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

این کار نوع Custom را مقداردهی اولیه می‌کند و تعدادی از اعضا را با مقادیر پیش‌فرض مناسب پر می‌کند؛ از جمله ob_type که در ابتدا آن را روی NULL قرار داده‌ایم.

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

این کار، نوع را به دیکشنری ماژول اضافه می‌کند. این به ما امکان می‌دهد نمونه‌های Custom را با فراخوانی کلاس Custom ایجاد کنیم:

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

همین است! تنها کار باقی‌مانده، ساختن آن است؛ کد بالا را در پرونده‌ای به نام custom.c قرار دهید،

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

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

در پرونده‌ای به نام pyproject.toml، و

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

در پرونده‌ای به نام setup.py؛ سپس با تایپ کردن

$ python -m pip install .

در یک پوسته باید پرونده‌ی custom.so را در یک زیرپوشه تولید کند و آن را نصب کند؛ اکنون پایتون را اجرا کنید --- باید بتوانید import custom را انجام دهید و با اشیای Custom آزمایش کنید.

این کار چندان سخت نبود، نه؟

البته، نوع Custom فعلی کاملاً بی‌جالب است. هیچ داده‌ای ندارد و هیچ کاری انجام نمی‌دهد. حتی نمی‌توان از آن زیرکلاس ساخت.

2.2. افزودن داده‌ها و متدها به مثال پایه

بیایید مثال پایه را گسترش دهیم تا برخی داده‌ها و متدها را اضافه کنیم. بیایید همچنین نوع را به‌گونه‌ای بسازیم که به‌عنوان کلاس پایه قابل استفاده باشد. ماژول جدیدی می‌سازیم، custom2، که این قابلیت‌ها را اضافه می‌کند:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for 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 رشته‌های پایتونی حاوی نام و نام خانوادگی هستند. ویژگی number یک عدد صحیح C است.

ساختار شیء متناسب با آن به‌روزرسانی می‌شود:

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);
}

که به عضو tp_dealloc انتساب داده می‌شود:

.tp_dealloc = Custom_dealloc,

این متد ابتدا شمارش ارجاع دو ویژگی پایتونی را پاک می‌کند. Py_XDECREF() به‌درستی حالتی را مدیریت می‌کند که آرگومان آن NULL باشد (که ممکن است در اینجا رخ دهد اگر tp_new در میانه کار شکست بخورد). سپس عضو tp_free از نوع شیء (که توسط Py_TYPE(self) محاسبه می‌شود) را فراخوانی می‌کند تا حافظه شیء را آزاد کند. توجه داشته باشید که نوع شیء ممکن است CustomType نباشد، زیرا شیء ممکن است نمونه‌ای از یک زیرکلاس باشد.

توجه

قالب‌ریزی صریح به CustomObject * در بالا لازم است، زیرا Custom_dealloc را طوری تعریف کردیم که یک آرگومان PyObject * بگیرد، چرا که اشاره‌گر تابع 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,

این کار روی همه‌ی معماری‌هایی که سی‌پایتون از آن‌ها پشتیبانی می‌کند، یکسان انجام می‌شود، اما طبق استاندارد C، موجب رفتار تعریف‌نشده (undefined behavior) می‌شود.

می‌خواهیم مطمئن شویم که نام و نام خانوادگی با رشته‌های خالی مقداردهی اولیه می‌شوند، بنابراین یک پیاده‌سازی 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 مسئول ایجاد (و نه مقداردهی اولیه‌ی) اشیاء از این نوع است. این هندلر در پایتون به‌صورت متد __new__() در دسترس قرار می‌گیرد. تعریف عضو tp_new الزامی نیست و در واقع بسیاری از نوع‌های توسعه‌ای صرفاً از PyType_GenericNew() بازاستفاده می‌کنند، همان‌طور که در نسخه‌ی اول نوع Custom در بالا انجام شد. در این حالت، از هندلر tp_new برای مقداردهی اولیه‌ی ویژگی‌های first و last به مقادیر پیش‌فرض غیر NULL استفاده می‌کنیم.

tp_new نوعی را که از آن نمونه‌سازی می‌شود (اگر از یک زیرکلاس نمونه‌سازی شود، لزوماً CustomType نیست) و هر آرگومانی را که هنگام فراخوانی نوع پاس داده شده است، دریافت می‌کند و انتظار می‌رود که نمونه‌ی ایجادشده را برگرداند. هندلرهای tp_new همیشه آرگومان‌های جایگاهی و کلیدواژه‌ای را می‌پذیرند، اما اغلب آرگومان‌ها را نادیده می‌گیرند و مدیریت آرگومان‌ها را به متدهای مقداردهنده‌ی اولیه (معروف به tp_init در C یا __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__() را فراخوانی می‌کند)، نباید سعی کنید در زمان اجرا با استفاده از ترتیب حل متد تعیین کنید که کدام متد باید فراخوانی شود. همیشه به‌صورت ایستا تعیین کنید که می‌خواهید کدام نوع را فراخوانی کنید و tp_new آن را مستقیماً یا از طریق type->tp_base->tp_new فراخوانی کنید. اگر این کار را نکنید، ممکن است زیرکلاس‌های پایتونی از نوع شما که از کلاس‌های دیگر تعریف‌شده در پایتون نیز ارث‌بری می‌کنند، به‌درستی کار نکنند. (به‌طور خاص، ممکن است نتوانید بدون دریافت 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 در پایتون به‌صورت متد __init__() در دسترس قرار می‌گیرد. از این متد برای مقداردهی اولیه‌ی شیء پس از ایجاد آن استفاده می‌شود. مقداردهنده‌های اولیه همیشه آرگومان‌های جایگاهی و کلیدواژه‌ای را می‌پذیرند و باید در صورت موفقیت 0 و در صورت خطا -1 را برگردانند.

برخلاف هندلر tp_new، هیچ تضمینی وجود ندارد که tp_init اصلاً فراخوانی شود (برای مثال، ماژول pickle به‌طور پیش‌فرض __init__() را روی نمونه‌های پیکل‌گشایی‌شده فراخوانی نمی‌کند). همچنین ممکن است چندین بار فراخوانی شود. هر کسی می‌تواند متد __init__() را روی اشیاء ما فراخوانی کند. به همین دلیل، باید هنگام انتساب مقادیر جدید به ویژگی‌ها بسیار مراقب باشیم. ممکن است، برای مثال، وسوسه شویم که عضو first را به این صورت انتساب دهیم:

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

اما این کار پرخطر خواهد بود. نوع ما نوع عضو first را محدود نمی‌کند، بنابراین می‌تواند هر نوع شیء باشد. ممکن است مخربی داشته باشد که موجب اجرای کدی شود که تلاش می‌کند به عضو first دسترسی پیدا کند؛ یا همان مخرب می‌تواند وضعیت نخ را جدا کند و اجازه دهد کد دلخواهی در نخ‌های دیگر اجرا شود که به شیء ما دسترسی پیدا می‌کند و آن را تغییر می‌دهد.

برای احتیاط افراطی و محافظت از خود در برابر این احتمال، تقریباً همیشه اعضا را پیش از کاهش شمارش ارجاعشان مجدداً انتساب می‌کنیم. چه زمانی لازم نیست این کار را انجام دهیم؟

  • وقتی به‌طور قطع می‌دانیم که شمارش ارجاع بزرگ‌تر از ۱ است؛

  • زمانی که می‌دانیم آزادسازی شیء [1] نه وضعیت نخ را جدا می‌کند و نه هیچ فراخوانی مجددی به کد نوع ما ایجاد می‌کند؛

  • هنگام کاهش شمارش ارجاع در هندلر 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 قرار گیرند.

ما یک متد واحد، 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);
}

این متد به‌صورت یک تابع C پیاده‌سازی شده است که یک نمونه‌ی Custom (یا زیرکلاس Custom) را به‌عنوان نخستین آرگومان می‌گیرد. متدها همیشه یک نمونه را به‌عنوان نخستین آرگومان می‌گیرند. متدها اغلب آرگومان‌های جایگاهی و کلیدواژه‌ای نیز می‌گیرند، اما در این مورد ما هیچ آرگومانی نمی‌گیریم و نیازی به پذیرفتن تاپل آرگومان‌های جایگاهی یا دیکشنری آرگومان‌های کلیدواژه‌ای نداریم. این متد معادل متد پایتونی زیر است:

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

توجه داشته باشید که باید احتمال NULL بودن اعضای first و last خود را بررسی کنیم. دلیل این است که این اعضا ممکن است حذف شوند، که در این صورت برابر NULL قرار می‌گیرند. بهتر است که از حذف این ویژگی‌ها جلوگیری کرده و مقادیر ویژگی‌ها را به رشته‌ها محدود کنیم. در بخش بعدی خواهیم دید که چگونه این کار را انجام دهیم.

اکنون که متد را تعریف کرده‌ایم، باید آرایه‌ای از تعریف‌های متد ایجاد کنیم:

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

(توجه کنید که از پرچم METH_NOARGS استفاده کردیم تا نشان دهیم که متد به‌جز self هیچ آرگومانی انتظار ندارد)

و آن را به جایگاه tp_methods اختصاص دهید:

.tp_methods = Custom_methods,

در نهایت، نوع خود را طوری می‌سازیم که به‌عنوان کلاس پایه برای زیرکلاس‌گیری قابل استفاده باشد. متدهای خود را تاکنون با دقت نوشته‌ایم تا هیچ فرضی درباره‌ی نوع شیء در حال ایجاد یا استفاده نداشته باشند؛ بنابراین تنها کاری که باید انجام دهیم این است که Py_TPFLAGS_BASETYPE را به تعریف پرچم کلاس خود اضافه کنیم:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,

نام PyInit_custom() را به 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 .

2.3. فراهم کردن کنترل دقیق‌تر بر ویژگی‌های داده

در این بخش، کنترل دقیق‌تری بر نحوه‌ی تنظیم ویژگی‌های first و last در مثال Custom ارائه خواهیم کرد. در نسخه‌ی قبلی ماژول ما، متغیرهای نمونه‌ی first و last می‌توانستند به مقادیری غیر از رشته تنظیم شوند یا حتی حذف شوند. می‌خواهیم مطمئن شویم که این ویژگی‌ها همیشه حاوی رشته باشند.

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for 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);
}

برای فراهم کردن کنترل بیشتر بر ویژگی‌های first و last، از توابع سفارشی 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 داشته باشیم که بر اساس داده‌های موجود در بستار تصمیم می‌گیرند کدام ویژگی را دریافت یا تنظیم کنند.)

به تابع تنظیم‌کننده، شیء Custom، مقدار جدید و بستار پاس داده می‌شود. مقدار جدید می‌تواند NULL باشد که در این صورت، ویژگی در حال حذف شدن است. در تنظیم‌کننده‌ی ما، اگر ویژگی حذف شود یا مقدار جدید آن رشته نباشد، خطایی مطرح می‌کنیم.

ما آرایه‌ای از ساختار‌های PyGetSetDef می‌سازیم:

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

و آن را در جایگاه tp_getset ثبت کنید:

.tp_getset = Custom_getsetters,

آخرین آیتم در ساختار PyGetSetDef همان «بستار» است که در بالا ذکر شد. در این حالت، ما از بستار استفاده نمی‌کنیم، بنابراین فقط NULL را ارسال می‌کنیم.

ما همچنین تعاریف اعضا را برای این ویژگی‌ها حذف می‌کنیم:

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

همچنین باید هندلر tp_init را به‌روزرسانی کنیم تا فقط رشته‌ها [3] به آن ارسال شوند:

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;
}

با این تغییرات، می‌توانیم اطمینان حاصل کنیم که اعضای first و last هرگز NULL نیستند؛ بنابراین می‌توانیم بررسی‌های مربوط به مقادیر NULL را در تقریباً همه‌ی موارد حذف کنیم. این بدان معناست که بیشتر فراخوانی‌های Py_XDECREF() را می‌توان به فراخوانی‌های Py_DECREF() تبدیل کرد. تنها جایی که نمی‌توانیم این فراخوانی‌ها را تغییر دهیم، پیاده‌سازی tp_dealloc است، که در آن این احتمال وجود دارد که مقداردهی اولیه‌ی این اعضا در tp_new شکست خورده باشد.

ما همچنین، همان‌طور که پیش‌تر انجام دادیم، نام تابع مقداردهی اولیه‌ی ماژول و نام ماژول در تابع مقداردهی اولیه را تغییر می‌دهیم و یک تعریف اضافی به پرونده‌ی setup.py می‌افزاییم.

2.4. پشتیبانی از زباله‌روبی چرخه‌ای

پایتون یک زباله‌روب چرخه‌ای (GC) دارد که می‌تواند اشیاء غیرلازم را حتی زمانی که شمارش ارجاع آن‌ها صفر نیست، شناسایی کند. این وضعیت می‌تواند هنگامی رخ دهد که اشیاء در چرخه‌ها درگیر باشند. برای مثال، در نظر بگیرید:

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

در این مثال، فهرستی ایجاد می‌کنیم که خودش را در بر می‌گیرد. وقتی آن را حذف می‌کنیم، همچنان ارجاعی از خودش به آن وجود دارد. شمارش ارجاع آن به صفر نمی‌رسد. خوشبختانه، زباله‌روب چرخه‌ای پایتون سرانجام درمی‌یابد که فهرست زباله است و آن را آزاد می‌کند.

در نسخه‌ی دوم از مثال Custom، اجازه دادیم هر نوع شیء در ویژگی‌های first یا last ذخیره شود [4]. علاوه بر این، در نسخه‌های دوم و سوم، اجازه دادیم که از Custom زیرکلاس‌سازی شود، و زیرکلاس‌ها می‌توانند ویژگی‌های دلخواه اضافه کنند. به‌خاطر هر یک از این دو دلیل، اشیای 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> /* for 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);
}

نخست، متد پیمایش به زباله‌روب چرخه‌ای اطلاع می‌دهد که کدام زیرشیءها ممکن است در چرخه‌ها شرکت کنند:

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 را که به متد پیمایش پاس داده شده است، به‌عنوان آرگومان می‌گیرد. این تابع یک مقدار عدد صحیح برمی‌گرداند که اگر غیرصفر باشد، باید بازگردانده شود.

پایتون یک ماکروی Py_VISIT() فراهم می‌کند که فراخوانی توابع بازدید (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;
}

توجه

پیاده‌سازی tp_traverse باید آرگومان‌های خود را دقیقاً visit و arg نام‌گذاری کند تا بتواند از Py_VISIT() استفاده کند.

دوم، باید متدی برای پاک کردن هر زیرشیئی که می‌تواند در چرخه‌ها شرکت کند ارائه دهیم:

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 فرض می‌کند شمارش ارجاع صفر نیست، باید پیش از پاک کردن اعضا، شیء را با فراخوانی 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 نوشته بودیم، باید آن‌ها را برای زباله‌روبی چرخه‌ای تغییر می‌دادیم. بیشتر ماژول‌های توسعه‌ای از نسخه‌هایی که به‌طور خودکار ارائه می‌شوند استفاده خواهند کرد.

2.5. زیرکلاس‌سازی نوع‌های دیگر

می‌توان نوع‌های توسعه‌ای جدیدی ایجاد کرد که از نوع‌های موجود مشتق شده‌اند. ساده‌ترین کار، ارث‌بری از نوع‌های توکار است، زیرا یک توسعه می‌تواند به‌سادگی از PyTypeObject مورد نیاز خود استفاده کند. اشتراک‌گذاری این ساختارهای PyTypeObject بین ماژول‌های توسعه‌ای می‌تواند دشوار باشد.

در این مثال یک نوع SubList ایجاد می‌کنیم که از نوع توکار list ارث‌بری می‌کند. نوع جدید کاملاً با فهرست‌های معمولی سازگار خواهد بود، اما یک متد اضافی 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() را در ابتدای ساختار خود در بر دارد.

وقتی یک شیء پایتون نمونه‌ای از 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_new و tp_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 باید در ساختار نوع پر شده باشد. هنگامی که از یک نوع موجود مشتق می‌گیریم، لازم نیست جایگاه tp_alloc با PyType_GenericNew() پر شود -- تابع تخصیص از نوع پایه به ارث می‌رسد.

پس از آن، فراخوانی PyType_Ready() و افزودن شیء نوع به ماژول، همانند مثال‌های پایه‌ی Custom است.

پانوشت‌ها