3. تعریف نوع‌های توسعه‌ای: موضوعات گوناگون

هدف این بخش، ارائه‌ی مروری سریع و گذرا بر متدهای مختلف نوع است؛ متدهایی که می‌توانید پیاده‌سازی کنید و اینکه هر یک چه کاری انجام می‌دهند.

در ادامه، تعریف PyTypeObject آمده است؛ برخی از فیلدهایی که تنها در ساخت‌های اشکال‌زدایی استفاده می‌شوند، از آن حذف شده‌اند:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;

    /* Method suites for standard classes */

    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More standard operations (here for binary compatibility) */

    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    PyMethodDef *tp_methods;
    PyMemberDef *tp_members;
    PyGetSetDef *tp_getset;
    // Strong reference on a heap type, borrowed reference on a static type
    PyTypeObject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache; /* no longer used */
    void *tp_subclasses;  /* for static builtin types this is an index */
    PyObject *tp_weaklist; /* not used for static builtin types */
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6.
     * If zero, the cache is invalid and must be initialized.
     */
    unsigned int tp_version_tag;

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;

    /* bitset of which type-watchers care about this type */
    unsigned char tp_watched;

    /* Number of tp_version_tag values used.
     * Set to _Py_ATTR_CACHE_UNUSED if the attribute cache is
     * disabled for this type (e.g. due to custom MRO entries).
     * Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
     */
    uint16_t tp_versions_used;
} PyTypeObject;

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

همان‌طور که احتمالاً تاکنون انتظار دارید، می‌خواهیم این موارد را مرور کنیم و اطلاعات بیشتری درباره هندلرهای گوناگون ارائه دهیم. به ترتیبی که در ساختار تعریف شده‌اند پیش نمی‌رویم، زیرا میراث تاریخی فراوانی وجود دارد که بر ترتیب فیلدها تأثیر می‌گذارد. اغلب ساده‌ترین راه این است که مثالی بیابید که فیلدهای مورد نیازتان را در بر داشته باشد و سپس مقادیر را متناسب با نوع جدید خود تغییر دهید.

const char *tp_name; /* برای چاپ */

نام نوع — همان‌طور که در فصل پیشین ذکر شد، این نام در مکان‌های مختلف ظاهر خواهد شد، تقریباً به‌طور کامل برای اهداف تشخیصی. سعی کنید چیزی را برگزینید که در چنین موقعیتی سودمند باشد!

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

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

const char *tp_doc;

در اینجا می‌توانید رشته‌ای (یا آدرس آن) را قرار دهید که می‌خواهید هنگامی که اسکریپت پایتون برای بازیابی رشته مستند به obj.__doc__ ارجاع می‌دهد، بازگردانده شود.

اکنون به متدهای پایه‌ی نوع می‌رسیم -- همان‌هایی که بیشتر نوع‌های توسعه‌ای آن‌ها را پیاده‌سازی خواهند کرد.

3.1. نهایی‌سازی و آزادسازی

destructor tp_dealloc;

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

static void
newdatatype_dealloc(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    free(self->obj_UnderlyingDatatypePtr);
    Py_TYPE(self)->tp_free(self);
}

اگر نوع شما از زباله‌روبی پشتیبانی کند، مخرب باید پیش از پاک کردن هر یک از فیلدهای عضو، PyObject_GC_UnTrack() را فراخوانی کند:

static void
newdatatype_dealloc(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    PyObject_GC_UnTrack(op);
    Py_CLEAR(self->other_obj);
    ...
    Py_TYPE(self)->tp_free(self);
}

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

static void
my_dealloc(PyObject *obj)
{
    MyObject *self = (MyObject *) obj;
    PyObject *cbresult;

    if (self->my_callback != NULL) {
        PyObject *err_type, *err_value, *err_traceback;

        /* This saves the current exception state */
        PyErr_Fetch(&err_type, &err_value, &err_traceback);

        cbresult = PyObject_CallNoArgs(self->my_callback);
        if (cbresult == NULL) {
           PyErr_WriteUnraisable(self->my_callback);
        }
        else {
            Py_DECREF(cbresult);
        }

        /* This restores the saved exception state */
        PyErr_Restore(err_type, err_value, err_traceback);

        Py_DECREF(self->my_callback);
    }
    Py_TYPE(self)->tp_free(self);
}

توجه

کارهایی که می‌توانید به‌طور ایمن در یک تابع آزادساز حافظه انجام دهید، محدودیت‌هایی دارد. نخست، اگر نوع شما از زباله‌روبی پشتیبانی کند (با استفاده از tp_traverse و/یا tp_clear)، ممکن است برخی از اعضای شیء تا زمانی که tp_dealloc فراخوانی می‌شود، پاک‌سازی یا نهایی‌سازی شده باشند. دوم، در tp_dealloc، شیء شما در وضعیتی ناپایدار قرار دارد: شمارش ارجاع آن برابر با صفر است. هر فراخوانی شیء یا API غیربدیهی (مانند مثال بالا) ممکن است در نهایت tp_dealloc را دوباره فراخوانی کند و موجب آزادسازی دوباره (double free) و فروپاشی شود.

از پایتون 3.4 به بعد، توصیه می‌شود هیچ کد نهایی‌سازی پیچیده‌ای در tp_dealloc قرار ندهید و در عوض از متد نوع جدید tp_finalize استفاده کنید.

همچنین ملاحظه نمائید

PEP 442 طرح جدید نهایی‌سازی را توضیح می‌دهد.

3.2. نمایش شیء

در پایتون، دو راه برای تولید نمایش متنی یک شیء وجود دارد: تابع repr() و تابع str(). (تابع print() فقط str() را فراخوانی می‌کند.) این هندلرها هر دو اختیاری هستند.

reprfunc tp_repr;
reprfunc tp_str;

هندلر tp_repr باید یک شیء رشته را برگرداند که حاوی نمایشی از نمونه‌ای باشد که برای آن فراخوانی می‌شود. در اینجا یک مثال ساده آمده است:

static PyObject *
newdatatype_repr(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}",
                                self->obj_UnderlyingDatatypePtr->size);
}

اگر هیچ هندلر tp_repr تعیین نشده باشد، مفسر بازنمایی‌ای را فراهم می‌کند که از tp_name نوع و مقداری که به‌طور یکتا شیء را شناسایی می‌کند، به کار می‌گیرد.

هندلر tp_str برای str() همان است که هندلر tp_repr که در بالا توضیح داده شد برای repr() است؛ یعنی زمانی فراخوانی می‌شود که کد پایتون str() را روی نمونه‌ای از شیء شما فراخوانی کند. پیاده‌سازی آن بسیار شبیه تابع tp_repr است، اما رشته‌ی حاصل برای خواندن توسط انسان در نظر گرفته شده است. اگر tp_str مشخص نشده باشد، از هندلر tp_repr استفاده می‌شود.

در اینجا یک مثال ساده آمده است:

static PyObject *
newdatatype_str(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}",
                                self->obj_UnderlyingDatatypePtr->size);
}

3.3. مدیریت ویژگی

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

پایتون از دو جفت هندلر ویژگی پشتیبانی می‌کند؛ نوعی که از ویژگی‌ها پشتیبانی می‌کند، تنها باید توابع یکی از این جفت‌ها را پیاده‌سازی کند. تفاوت این است که یک جفت، نام ویژگی را به‌صورت char* می‌گیرد، در حالی که جفت دیگر PyObject* را می‌پذیرد. هر نوع می‌تواند از هر جفتی که برای راحتی پیاده‌سازی منطقی‌تر باشد استفاده کند.

getattrfunc  tp_getattr;        /* char * version */
setattrfunc  tp_setattr;
/* ... */
getattrofunc tp_getattro;       /* PyObject * version */
setattrofunc tp_setattro;

اگر دسترسی به ویژگی‌های یک شیء همیشه یک عملیات ساده باشد (این موضوع در ادامه توضیح داده خواهد شد)، پیاده‌سازی‌های عامی وجود دارند که می‌توان از آن‌ها برای ارائه نسخه‌ی PyObject* از توابع مدیریت ویژگی استفاده کرد. نیاز واقعی به هندلرهای ویژگی مخصوص نوع، از پایتون 2.2 به بعد تقریباً به‌طور کامل از بین رفت، هرچند مثال‌های زیادی برای استفاده از برخی از سازوکارهای عام جدیدِ موجود به‌روزرسانی نشده‌اند.

3.3.1. مدیریت عام ویژگی

بیشتر نوع‌های توسعه‌ای تنها از ویژگی‌های ساده استفاده می‌کنند. پس، چه چیزی ویژگی‌ها را ساده می‌کند؟ تنها چند شرط وجود دارد که باید برآورده شوند:

  1. نام ویژگی‌ها باید هنگامی که PyType_Ready() فراخوانی می‌شود، مشخص باشد.

  2. هیچ پردازش خاصی برای ثبت این‌که ویژگی‌ای جستجو یا تنظیم شده است لازم نیست، و هیچ کنشی نیز بر اساس مقدار لازم نیست انجام شود.

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

وقتی PyType_Ready() فراخوانی می‌شود، از سه جدولی که شیء نوع به آن‌ها ارجاع می‌دهد برای ایجاد توصیف‌گرهایی که در دیکشنری شیء نوع قرار می‌گیرند استفاده می‌کند. هر توصیف‌گر دسترسی به یک ویژگی از شیء نمونه را کنترل می‌کند. هر یک از این جدول‌ها اختیاری است؛ اگر هر سه NULL باشند، نمونه‌های این نوع فقط ویژگی‌هایی را خواهند داشت که از نوع پایه خود به ارث برده‌اند، و فیلدهای tp_getattro و tp_setattro نیز باید NULL باقی بمانند تا نوع پایه بتواند ویژگی‌ها را مدیریت کند.

جدول‌ها به‌عنوان سه فیلد از شیء نوع اعلان می‌شوند:

struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;

اگر tp_methods برابر NULL نباشد، باید به آرایه‌ای از ساختارهای PyMethodDef اشاره کند. هر ورودی جدول نمونه‌ای از این ساختار است:

typedef struct PyMethodDef {
    const char  *ml_name;       /* method name */
    PyCFunction  ml_meth;       /* implementation function */
    int          ml_flags;      /* flags */
    const char  *ml_doc;        /* docstring */
} PyMethodDef;

برای هر متدی که توسط نوع ارائه می‌شود، باید یک ورودی تعریف شود؛ برای متدهایی که از یک نوع پایه به ارث رسیده‌اند، نیازی به ورودی نیست. در انتها، به یک ورودی اضافی نیاز است؛ این ورودی یک نشانگر است که پایان آرایه را مشخص می‌کند. فیلد ml_name نشانگر باید NULL باشد.

از جدول دوم برای تعریف ویژگی‌هایی استفاده می‌شود که مستقیماً به داده‌های ذخیره‌شده در نمونه نگاشت می‌شوند. انواع مختلفی از نوع‌های اولیه‌ی C پشتیبانی می‌شوند و دسترسی می‌تواند فقط‌خواندنی یا خواندنی-نوشتنی باشد. ساختارهای موجود در جدول به صورت زیر تعریف می‌شوند:

typedef struct PyMemberDef {
    const char *name;
    int         type;
    int         offset;
    int         flags;
    const char *doc;
} PyMemberDef;

برای هر ورودی در جدول، یک توصیف‌گر ساخته و به نوع اضافه می‌شود که قادر خواهد بود مقداری را از ساختار نمونه استخراج کند. فیلد type باید حاوی کد نوعی مانند Py_T_INT یا Py_T_DOUBLE باشد؛ این مقدار برای تعیین نحوه‌ی تبدیل مقادیر پایتون به مقادیر C و بالعکس استفاده خواهد شد. فیلد flags برای ذخیره‌ی پرچم‌هایی استفاده می‌شود که نحوه‌ی دسترسی به ویژگی را کنترل می‌کنند: می‌توانید آن را روی Py_READONLY تنظیم کنید تا کد پایتون نتواند آن را مقداردهی کند.

مزیت جالب استفاده از جدول tp_members برای ساخت توصیف‌گرهایی که در ران‌تایم استفاده می‌شوند این است که هر ویژگی که به این روش تعریف شود، می‌تواند صرفاً با ارائه‌ی متن در جدول، یک رشته مستند مرتبط داشته باشد. یک برنامه می‌تواند از API درون‌نگری برای بازیابی توصیف‌گر از شیء کلاس استفاده کند و رشته مستند را با استفاده از ویژگی __doc__ آن به دست آورد.

مانند جدول tp_methods، یک ورودی نشانگر که مقدار ml_name آن NULL است، لازم است.

3.3.2. مدیریت ویژگی‌های مخصوص به نوع

برای سادگی، در اینجا فقط نسخه‌ی char* نمایش داده خواهد شد؛ نوع پارامتر name تنها تفاوت بین گونه‌های char* و PyObject* رابط است. این مثال عملاً همان کاری را انجام می‌دهد که مثال عام بالا انجام می‌دهد، اما از پشتیبانی عام افزوده‌شده در Python 2.2 استفاده نمی‌کند. این مثال توضیح می‌دهد که توابع هندلر چگونه فراخوانی می‌شوند، تا اگر به‌راستی نیاز به گسترش قابلیت‌های آن‌ها داشته باشید، بفهمید چه کاری باید انجام شود.

هندلر tp_getattr زمانی فراخوانی می‌شود که شیء به جست‌وجوی ویژگی نیاز داشته باشد. این هندلر در همان موقعیت‌هایی فراخوانی می‌شود که متد __getattr__() یک کلاس فراخوانی می‌شود.

در ادامه یک مثال آمده است:

static PyObject *
newdatatype_getattr(PyObject *op, char *name)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    if (strcmp(name, "data") == 0) {
        return PyLong_FromLong(self->data);
    }

    PyErr_Format(PyExc_AttributeError,
                 "'%.100s' object has no attribute '%.400s'",
                 Py_TYPE(self)->tp_name, name);
    return NULL;
}

هندلر tp_setattr زمانی فراخوانی می‌شود که قرار باشد متد __setattr__() یا __delattr__() یک نمونه‌ی کلاس فراخوانی شود. وقتی ویژگی‌ای باید حذف شود، پارامتر سوم NULL خواهد بود. در ادامه مثالی آمده است که صرفاً یک استثنا ایجاد می‌کند؛ اگر واقعاً همین تمام چیزی بود که می‌خواستید، هندلر tp_setattr باید روی NULL تنظیم شود.

static int
newdatatype_setattr(PyObject *op, char *name, PyObject *v)
{
    PyErr_Format(PyExc_RuntimeError, "Read-only attribute: %s", name);
    return -1;
}

3.4. مقایسه‌ی شیء

richcmpfunc tp_richcompare;

هندلر tp_richcompare زمانی فراخوانی می‌شود که به مقایسه نیاز باشد. این هندلر مشابه متدهای مقایسه‌ی غنی (rich comparison) مانند __lt__() است و همچنین توسط PyObject_RichCompare() و PyObject_RichCompareBool() فراخوانی می‌شود.

این تابع با دو شیء پایتون و عملگر به‌عنوان آرگومان فراخوانی می‌شود، که در آن عملگر یکی از Py_EQ، Py_NE، Py_LE، Py_GE، Py_LT یا Py_GT است. این تابع باید دو شیء را با توجه به عملگر مشخص‌شده مقایسه کند و در صورت موفقیت مقایسه، Py_True یا Py_False را برگرداند، Py_NotImplemented را برای نشان دادن این‌که مقایسه پیاده‌سازی نشده و باید متد مقایسه شیء دیگر امتحان شود، یا NULL را در صورتی که استثنایی تنظیم شده باشد.

در اینجا یک پیاده‌سازی نمونه برای یک نوع داده آمده است که در صورتی برابر در نظر گرفته می‌شود که اندازه‌ی اشاره‌گر داخلی‌اش برابر باشد:

static PyObject *
newdatatype_richcmp(PyObject *lhs, PyObject *rhs, int op)
{
    newdatatypeobject *obj1 = (newdatatypeobject *) lhs;
    newdatatypeobject *obj2 = (newdatatypeobject *) rhs;
    PyObject *result;
    int c, size1, size2;

    /* code to make sure that both arguments are of type
       newdatatype omitted */

    size1 = obj1->obj_UnderlyingDatatypePtr->size;
    size2 = obj2->obj_UnderlyingDatatypePtr->size;

    switch (op) {
    case Py_LT: c = size1 <  size2; break;
    case Py_LE: c = size1 <= size2; break;
    case Py_EQ: c = size1 == size2; break;
    case Py_NE: c = size1 != size2; break;
    case Py_GT: c = size1 >  size2; break;
    case Py_GE: c = size1 >= size2; break;
    }
    result = c ? Py_True : Py_False;
    return Py_NewRef(result);
 }

3.5. پشتیبانی از پروتکل انتزاعی

پایتون انواع مختلفی از «پروتکل‌های انتزاعی» را پشتیبانی می‌کند؛ رابط‌های مشخصی که برای استفاده از این رابط‌ها فراهم شده‌اند، در لایه اشیاء انتزاعی مستند شده‌اند.

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

PyNumberMethods   *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods  *tp_as_mapping;

اگر می‌خواهید شیء شما بتواند مانند یک عدد، یک دنباله یا یک شیء نگاشت رفتار کند، آدرس ساختاری را که به‌ترتیب نوع C PyNumberMethods، PySequenceMethods یا PyMappingMethods را پیاده‌سازی می‌کند، قرار می‌دهید. پر کردن این ساختار با مقدارهای مناسب بر عهده شماست. می‌توانید نمونه‌هایی از استفاده از هر یک از این‌ها را در پوشه Objects توزیع کد منبع پایتون بیابید.

hashfunc tp_hash;

این تابع، اگر بخواهید آن را ارائه کنید، باید یک عدد هش برای نمونه‌ای از نوع داده‌ی شما بازگرداند. در اینجا یک مثال ساده آمده است:

static Py_hash_t
newdatatype_hash(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    Py_hash_t result;
    result = self->some_size + 32767 * self->some_number;
    if (result == -1) {
        result = -2;
    }
    return result;
}

Py_hash_t یک نوع عدد صحیح علامت‌دار با پهنای متغیر بر اساس پلتفرم است. برگرداندن -1 از tp_hash نشان‌دهنده‌ی خطا است؛ به همین دلیل باید مراقب باشید تا در صورت موفقیت‌آمیز بودن محاسبه‌ی هش، آن را برنگردانید، همان‌طور که در بالا مشاهده کردید.

ternaryfunc tp_call;

این تابع زمانی فراخوانی می‌شود که نمونه‌ای از نوع داده شما «فراخوانی» شود؛ برای مثال، اگر obj1 نمونه‌ای از نوع داده شما باشد و اسکریپت پایتون شامل obj1('hello') باشد، هندلر tp_call فراخوانی می‌شود.

این تابع سه آرگومان می‌گیرد:

  1. self نمونه‌ای از نوع داده است که موضوع فراخوانی است. اگر فراخوانی obj1('hello') باشد، آنگاه self همان obj1 است.

  2. args تاپلی است که آرگومان‌های فراخوانی را در بر دارد. می‌توانید از PyArg_ParseTuple() برای استخراج آرگومان‌ها استفاده کنید.

  3. kwds یک دیکشنری از آرگومان‌های کلیدواژه‌ای است که ارسال شده‌اند. اگر این مقدار غیر NULL باشد و شما از آرگومان‌های کلیدواژه‌ای پشتیبانی می‌کنید، برای استخراج آرگومان‌ها از PyArg_ParseTupleAndKeywords() استفاده کنید. اگر نمی‌خواهید از آرگومان‌های کلیدواژه‌ای پشتیبانی کنید و این مقدار غیر NULL باشد، یک TypeError با پیامی که می‌گوید آرگومان‌های کلیدواژه‌ای پشتیبانی نمی‌شوند ایجاد کنید.

در ادامه، یک پیاده‌سازی ساده از tp_call آمده است:

static PyObject *
newdatatype_call(PyObject *op, PyObject *args, PyObject *kwds)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    PyObject *result;
    const char *arg1;
    const char *arg2;
    const char *arg3;

    if (!PyArg_ParseTuple(args, "sss:call", &arg1, &arg2, &arg3)) {
        return NULL;
    }
    result = PyUnicode_FromFormat(
        "Returning -- value: [%d] arg1: [%s] arg2: [%s] arg3: [%s]\n",
        self->obj_UnderlyingDatatypePtr->size,
        arg1, arg2, arg3);
    return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

این توابع از پروتکل پیمایش‌گر پشتیبانی می‌کنند. هر دو هندلر دقیقاً یک پارامتر می‌گیرند — نمونه‌ای که برای آن فراخوانی می‌شوند — و یک ارجاع جدید برمی‌گردانند. در صورت خطا، باید یک استثنا تنظیم کنند و NULL را برگردانند. tp_iter با متد __iter__() پایتون متناظر است، در حالی که tp_iternext با متد __next__() پایتون متناظر است.

هر شیء iterable باید هندلر tp_iter را پیاده‌سازی کند که باید یک شیء iterator را برگرداند. در اینجا همان دستورالعمل‌های مربوط به کلاس‌های پایتون اعمال می‌شود:

  • برای کلکسیون‌هایی (مانند فهرست‌ها و تاپل‌ها) که می‌توانند از چندین پیمایش‌گر مستقل پشتیبانی کنند، باید با هر فراخوانی tp_iter یک پیمایش‌گر جدید ایجاد و برگردانده شود.

  • اشیایی که تنها یک بار می‌توان آن‌ها را پیمایش کرد (معمولاً به دلیل اثرات جانبی پیمایش، مانند اشیای پرونده) می‌توانند tp_iter را با بازگرداندن یک ارجاع جدید به خودشان پیاده‌سازی کنند -- و بنابراین باید هندلر tp_iternext را نیز پیاده‌سازی کنند.

هر شیء iterator باید هم tp_iter و هم tp_iternext را پیاده‌سازی کند. هندلر tp_iter در یک پیمایش‌گر باید ارجاع جدیدی به پیمایش‌گر برگرداند. هندلر tp_iternext آن باید در صورت وجود، ارجاع جدیدی به شیء بعدی در پیمایش برگرداند. اگر پیمایش به پایان رسیده باشد، tp_iternext می‌تواند بدون تنظیم استثنا NULL برگرداند، یا علاوه بر برگرداندن NULL، StopIteration را تنظیم کند؛ پرهیز از استثنا می‌تواند به کارایی کمی بهتر منجر شود. اگر خطای واقعی رخ دهد، tp_iternext باید همیشه یک استثنا تنظیم کند و NULL برگرداند.

3.6. پشتیبانی از ارجاع ضعیف

یکی از اهداف پیاده‌سازی ارجاع ضعیف در پایتون این است که به هر نوعی اجازه دهد در سازوکار ارجاع ضعیف شرکت کند، بدون آنکه سرباری بر شیءهای حساس به کارایی (مانند اعداد) تحمیل شود.

همچنین ملاحظه نمائید

مستندات ماژول weakref.

برای اینکه بتوان به یک شیء ارجاع ضعیف داد، نوع توسعه‌ای باید بیت Py_TPFLAGS_MANAGED_WEAKREF را در فیلد tp_flags تنظیم کند. فیلد قدیمی tp_weaklistoffset باید صفر باقی بماند.

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

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... سایر اعضا برای اختصار حذف شده‌اند ... */
    .tp_flags = Py_TPFLAGS_MANAGED_WEAKREF | ...,
};

تنها مورد دیگری که باید اضافه شود این است که tp_dealloc باید هر ارجاع ضعیفی را پاک کند (با فراخوانی PyObject_ClearWeakRefs()):

static void
Trivial_dealloc(PyObject *op)
{
    /* Clear weakrefs first before calling any destructors */
    PyObject_ClearWeakRefs(op);
    /* ... remainder of destruction code omitted for brevity ... */
    Py_TYPE(op)->tp_free(op);
}

3.7. پیشنهادهای بیشتر

برای یادگیری نحوه‌ی پیاده‌سازی هر متد ویژه برای نوع داده‌ی جدید خود، کد منبع CPython را دریافت کنید. به پوشه‌ی Objects بروید، سپس در پرونده‌های منبع C به دنبال tp_ به‌علاوه‌ی تابعی که می‌خواهید بگردید (برای مثال، tp_richcompare). نمونه‌هایی از تابعی که می‌خواهید پیاده‌سازی کنید را خواهید یافت.

هنگامی که نیاز دارید بررسی کنید که یک شیء، نمونه‌ی عینی نوعی است که پیاده‌سازی می‌کنید، از تابع PyObject_TypeCheck() استفاده کنید. نمونه‌ای از کاربرد آن می‌تواند چیزی شبیه به موارد زیر باشد:

if (!PyObject_TypeCheck(some_object, &MyType)) {
    PyErr_SetString(PyExc_TypeError, "arg #1 not a mything");
    return NULL;
}

همچنین ملاحظه نمائید

دانلود نسخه‌های کد منبع سی‌پایتون.

https://www.python.org/downloads/source/

پروژه‌ی سی‌پایتون در GitHub، جایی که کد منبع سی‌پایتون در آن توسعه داده می‌شود.

https://github.com/python/cpython