Using the C API: Assorted topics

The tutorial walked you through creating a C API extension module, but left many areas unexplained. This document looks at several concepts that you'll need to learn in order to write more complex extensions.

Errors and Exceptions

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

API پایتون تعدادی تابع برای تنظیم انواع مختلف استثناها تعریف می‌کند.

رایج‌ترین آن‌ها PyErr_SetString() است. آرگومان‌های آن یک شیء استثنا و یک رشته C هستند. شیء استثنا معمولاً شیء از پیش تعریف‌شده‌ای مانند PyExc_ZeroDivisionError است. رشته C علت خطا را نشان می‌دهد، به یک شیء رشته پایتون تبدیل می‌شود و به عنوان «مقدار مرتبط» استثنا ذخیره می‌شود.

تابع مفید دیگر PyErr_SetFromErrno() است که تنها یک آرگومان استثنا می‌گیرد و مقدار مرتبط را با بررسی متغیر سراسری errno می‌سازد. عمومی‌ترین تابع PyErr_SetObject() است که دو آرگومان شیء می‌گیرد: استثنا و مقدار مرتبط با آن. نیازی نیست اشیاء ارسال‌شده به هیچ‌یک از این توابع را Py_INCREF() کنید.

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

وقتی تابعی f که تابع دیگری به نام g را فراخوانی می‌کند تشخیص می‌دهد که آن تابع شکست خورده است، f باید خودش یک مقدار خطا برگرداند (معمولاً NULL یا -1). این تابع نباید یکی از توابع PyErr_* را فراخوانی کند --- یکی از آن‌ها قبلاً توسط g فراخوانی شده است. سپس فراخوان‌کننده f نیز باید نشانه‌ای از خطا را به فراخوان‌کننده خودش برگرداند، باز هم بدون فراخوانی PyErr_*، و به همین ترتیب --- دقیق‌ترین علت خطا قبلاً توسط تابعی که نخست آن را تشخیص داده است گزارش شده است. وقتی خطا به حلقه اصلی مفسر پایتون می‌رسد، این امر کد پایتون در حال اجرا را متوقف می‌کند و تلاش می‌کند هندلر استثنایی را که برنامه‌نویس پایتون تعیین کرده است بیابد.

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

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

هر فراخوانی ناموفق malloc() باید به یک استثنا تبدیل شود --- فراخوانی‌کننده‌ی مستقیم malloc() (یا realloc()) باید PyErr_NoMemory() را فراخوانی کند و خودش نشانگر شکست را برگرداند. همه‌ی توابع ایجادکننده‌ی شیء (برای مثال، PyLong_FromLong()) از قبل این کار را انجام می‌دهند، بنابراین این نکته فقط به کسانی مربوط است که malloc() را مستقیماً فراخوانی می‌کنند.

همچنین توجه داشته باشید که، با استثنای مهم PyArg_ParseTuple() و امثال آن، توابعی که یک وضعیت عدد صحیح برمی‌گردانند معمولاً مانند فراخوانی‌های سیستمی یونیکس، برای موفقیت مقداری مثبت یا صفر و برای شکست -1 برمی‌گردانند.

در نهایت، هنگامی که نشانگر خطا را برمی‌گردانید، مراقب باشید که زباله‌ها را پاک‌سازی کنید (با انجام فراخوانی‌های Py_XDECREF() یا Py_DECREF() برای اشیایی که از قبل ایجاد کرده‌اید)!

انتخاب اینکه کدام استثنا را برافرازید، کاملاً با شماست. اشیاء C از پیش اعلان‌شده‌ای متناظر با تمام استثناهای توکار پایتون وجود دارند، مانند PyExc_ZeroDivisionError، که می‌توانید مستقیماً از آن‌ها استفاده کنید. البته، باید استثناها را هوشمندانه انتخاب کنید --- از PyExc_TypeError برای بیان اینکه پرونده‌ای نتوانست باز شود استفاده نکنید (آن مورد احتمالاً باید PyExc_OSError باشد). اگر مشکلی در فهرست آرگومان‌ها وجود داشته باشد، تابع PyArg_ParseTuple() معمولاً استثنای PyExc_TypeError را برمی‌افرازد. اگر آرگومانی داشته باشید که مقدار آن باید در محدوده‌ی خاصی باشد یا باید شرایط دیگری را برآورده کند، PyExc_ValueError مناسب است.

شما همچنین می‌توانید یک استثنای جدید تعریف کنید که مختص ماژول شما باشد. ساده‌ترین راه برای انجام این کار، اعلان یک متغیر شیء سراسری ایستا در ابتدای پرونده است:

static PyObject *SpamError = NULL;

و آن را با فراخوانی PyErr_NewException() در تابع Py_mod_exec ماژول (spam_module_exec()) مقداردهی اولیه کنید:

SpamError = PyErr_NewException("spam.error", NULL, NULL);

از آنجا که SpamError یک متغیر سراسری است، هر بار که ماژول مجدداً مقداردهی اولیه می‌شود — یعنی هنگامی که تابع Py_mod_exec فراخوانی می‌شود — بازنویسی خواهد شد.

فعلاً، اجازه دهید از این مسئله اجتناب کنیم: راه‌اندازی مکرر را با پرتاب ImportError مسدود خواهیم کرد:

static PyObject *SpamError = NULL;

static int
spam_module_exec(PyObject *m)
{
    if (SpamError != NULL) {
        PyErr_SetString(PyExc_ImportError,
                        "cannot initialize spam module more than once");
        return -1;
    }
    SpamError = PyErr_NewException("spam.error", NULL, NULL);
    if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot spam_module_slots[] = {
    {Py_mod_exec, spam_module_exec},
    {0, NULL}
};

static struct PyModuleDef spam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    .m_size = 0,  // non-negative
    .m_slots = spam_module_slots,
};

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}

توجه داشته باشید که نام پایتونی شیء استثنا spam.error است. تابع PyErr_NewException() می‌تواند کلاسی ایجاد کند که کلاس پایه‌ی آن Exception باشد (مگر اینکه کلاس دیگری به جای NULL ارسال شود)؛ این کلاس پایه در استثناهای توکار توصیف شده است.

همچنین توجه داشته باشید که متغیر SpamError ارجاعی به کلاس استثنای به‌تازگی ایجادشده را نگه می‌دارد؛ این کار عمدی است! از آنجا که ممکن است کد خارجی استثنا را از ماژول حذف کند، به یک ارجاع مالکانه (owned reference) به کلاس نیاز است تا اطمینان حاصل شود که کلاس دور انداخته نمی‌شود و در نتیجه SpamError به اشاره‌گر سرگردان (dangling pointer) تبدیل نمی‌شود. اگر این اشاره‌گر سرگردان شود، کد C که استثنا را برمی‌انگیزد ممکن است باعث برون‌ریزی هسته یا سایر اثرات جانبی ناخواسته شود.

در حال حاضر، فراخوانی Py_DECREF() برای حذف این ارجاع وجود ندارد. حتی هنگام خاموش شدن مفسر پایتون، متغیر سراسری SpamError زباله‌روبی نخواهد شد. این متغیر «نشت» خواهد کرد. با این حال، اطمینان حاصل کردیم که این اتفاق حداکثر یک بار به ازای هر فرایند رخ خواهد داد.

استفاده از PyMODINIT_FUNC به‌عنوان نوع بازگشتی تابع را در ادامه‌ی این نمونه بحث می‌کنیم.

می‌توانید استثنای spam.error را در ماژول توسعه‌ای خود با فراخوانی PyErr_SetString() ایجاد کنید، همان‌طور که در ادامه نشان داده شده است:

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = system(command);
    if (sts < 0) {
        PyErr_SetString(SpamError, "System command failed");
        return NULL;
    }
    return PyLong_FromLong(sts);
}

Embedding an extension

If you want to make your module a permanent part of the Python interpreter, you will have to change the configuration setup and rebuild the interpreter. On Unix, place your file (spammodule.c for example) in the Modules/ directory of an unpacked source distribution, add a line to the file Modules/Setup.local describing your file:

spam spammodule.o

و مفسر را با اجرای make در پوشه‌ی سطح بالا بازسازی کنید. همچنین می‌توانید make را در زیرپوشه‌ی Modules/ اجرا کنید، اما در این صورت باید ابتدا Makefile را در آنجا با اجرای 'make Makefile' بازسازی کنید. (این کار هر بار که پرونده‌ی Setup را تغییر می‌دهید، ضروری است.)

اگر ماژول شما به کتابخانه‌های اضافی برای پیوند دادن نیاز دارد، می‌توانید آن‌ها را نیز در همان سطر در پرونده پیکربندی فهرست کنید، برای مثال:

spam spammodule.o -lX11

فراخوانی توابع پایتون از زبان C

The tutorial concentrated on making C functions callable from Python. The reverse is also useful: calling Python functions from C. This is especially the case for libraries that support so-called "callback" functions. If a C interface makes use of callbacks, the equivalent Python often needs to provide a callback mechanism to the Python programmer; the implementation will require calling the Python callback functions from a C callback. Other uses are also imaginable.

خوشبختانه، مفسر پایتون به‌راحتی به‌صورت بازگشتی فراخوانی می‌شود و یک رابط استاندارد برای فراخوانی یک تابع پایتون وجود دارد. (اگر می‌خواهید بدانید چگونه می‌توان پارسر پایتون را با یک رشته خاص به‌عنوان ورودی فراخوانی کرد، به لایه سطح بسیار بالا مراجعه کنید.)

فراخوانی یک تابع پایتون آسان است. نخست، برنامه پایتون باید به نحوی شیء تابع پایتون را به شما پاس دهد. شما باید تابعی (یا رابط دیگری) برای انجام این کار فراهم کنید. وقتی این تابع فراخوانی شد، اشاره‌گری به شیء تابع پایتون را (مراقب باشید که آن را Py_INCREF() کنید!) در یک متغیر سراسری --- یا هر جای دیگری که صلاح می‌دانید --- ذخیره کنید. برای مثال، تابع زیر ممکن است بخشی از تعریف یک ماژول باشد:

static PyObject *my_callback = NULL;

static PyObject *
my_set_callback(PyObject *dummy, PyObject *args)
{
    PyObject *result = NULL;
    PyObject *temp;

    if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError, "parameter must be callable");
            return NULL;
        }
        Py_XINCREF(temp);         /* Add a reference to new callback */
        Py_XDECREF(my_callback);  /* Dispose of previous callback */
        my_callback = temp;       /* Remember new callback */
        /* Boilerplate to return "None" */
        Py_INCREF(Py_None);
        result = Py_None;
    }
    return result;
}

This function must be registered with the interpreter using the METH_VARARGS flag in PyMethodDef.ml_flags. The PyArg_ParseTuple() function and its arguments are documented in section استخراج پارامترها در توابع توسعه‌ای.

ماکروهای Py_XINCREF() و Py_XDECREF() شمارش ارجاع یک شیء را افزایش/کاهش می‌دهند و در صورت وجود اشاره‌گرهای NULL ایمن هستند (اما توجه داشته باشید که temp در این زمینه NULL نخواهد بود). اطلاعات بیشتر درباره‌ی آن‌ها در بخش شمارش ارجاع آمده است.

بعداً، وقتی زمان فراخوانی تابع فرا می‌رسد، شما تابع Cِ PyObject_CallObject() را فراخوانی می‌کنید. این تابع دو آرگومان دارد که هر دو اشاره‌گر به اشیاء پایتونی دلخواه هستند: تابع پایتونی و فهرست آرگومان‌ها. فهرست آرگومان‌ها باید همیشه یک شیء تاپل باشد که طول آن برابر با تعداد آرگومان‌هاست. برای فراخوانی تابع پایتونی بدون هیچ آرگومانی، NULL یا یک تاپل خالی را ارسال کنید؛ برای فراخوانی آن با یک آرگومان، یک تاپل تک‌نمونه ارسال کنید. Py_BuildValue() زمانی که رشته قالب آن از صفر یا چند کد قالب درون پرانتز تشکیل شده باشد، یک تاپل برمی‌گرداند. برای مثال:

int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);

PyObject_CallObject() یک اشاره‌گر به شیء پایتون برمی‌گرداند: این همان مقدار بازگشتی تابع پایتون است. PyObject_CallObject() نسبت به آرگومان‌های خود، از نظر شمارش ارجاع خنثی (reference-count-neutral) است. در این مثال، یک تاپل جدید برای استفاده به‌عنوان فهرست آرگومان‌ها ساخته می‌شود که بلافاصله پس از فراخوانی PyObject_CallObject()، Py_DECREF() روی آن اعمال می‌شود.

مقدار بازگشتی PyObject_CallObject() «جدید» است: یا شیئی کاملاً جدید است، یا شیئی موجود است که شمارش ارجاع آن افزایش یافته است. بنابراین، مگر آنکه بخواهید آن را در یک متغیر سراسری ذخیره کنید، باید به نحوی نتیجه را Py_DECREF() کنید، حتی (به‌ویژه!) اگر علاقه‌ای به مقدار آن نداشته باشید.

با این حال، پیش از انجام این کار، مهم است که بررسی کنید مقدار بازگشتی NULL نیست. اگر چنین باشد، تابع پایتونی با ایجاد یک استثنا خاتمه یافته است. اگر کد C که PyObject_CallObject() را فراخوانی کرده است از پایتون فراخوانی شده باشد، باید اکنون نشان خطایی را به فراخواننده پایتونی خود بازگرداند تا مفسر بتواند ردگیری پشته را چاپ کند، یا کد پایتونی فراخواننده بتواند استثنا را مدیریت کند. اگر این کار ممکن یا مطلوب نباشد، استثنا باید با فراخوانی PyErr_Clear() پاک شود. برای مثال:

if (result == NULL)
    return NULL; /* Pass error back */
...use result...
Py_DECREF(result);

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

PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
    return NULL; /* بازگرداندن خطا */
/* اینجا شاید از نتیجه استفاده شود */
Py_DECREF(result);

به قرارگیری Py_DECREF(arglist) بلافاصله پس از فراخوانی و پیش از بررسی خطا توجه کنید! همچنین توجه داشته باشید که به‌طور دقیق این کد کامل نیست: ممکن است Py_BuildValue() با کمبود حافظه مواجه شود و این موضوع باید بررسی شود.

شما همچنین می‌توانید با استفاده از PyObject_Call() که از آرگومان‌ها و آرگومان‌های کلیدواژه‌ای پشتیبانی می‌کند، تابعی را با آرگومان‌های کلیدواژه‌ای فراخوانی کنید. مانند مثال بالا، از Py_BuildValue() برای ساخت دیکشنری استفاده می‌کنیم.

PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
    return NULL; /* بازگرداندن خطا */
/* اینجا شاید از نتیجه استفاده کنید */
Py_DECREF(result);

استخراج پارامترها در توابع توسعه‌ای

The tutorial uses a "METH_O" function, which is limited to a single Python argument. If you want more, you can use METH_VARARGS instead. With this flag, the C function will receive a tuple of arguments instead of a single object.

For unpacking the tuple, CPython provides the PyArg_ParseTuple() function, declared as follows:

int PyArg_ParseTuple(PyObject *arg, const char *format, ...);

آرگومان arg باید یک شیء تاپل حاوی فهرست آرگومانی باشد که از پایتون به یک تابع C پاس داده می‌شود. آرگومان format باید یک رشته قالب باشد که سینتکس آن در تجزیه آرگومان‌ها و ساخت مقادیر در راهنمای مرجع Python/C API توضیح داده شده است. آرگومان‌های باقی‌مانده باید آدرس‌های متغیرهایی باشند که نوع آن‌ها توسط رشته قالب تعیین می‌شود.

For example, to receive a single Python str object and turn it into a C buffer, you would use "s" as the format string:

const char *command;
if (!PyArg_ParseTuple(args, "s", &command)) {
    return NULL;
}

If an error is detected in the argument list, PyArg_ParseTuple() returns NULL (the error indicator for functions returning object pointers); your function may return NULL, relying on the exception set by PyArg_ParseTuple().

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

توجه داشته باشید که هر ارجاع به شیء پایتون که در اختیار فراخواننده قرار می‌گیرد، یک ارجاع امانتی است؛ شمارش ارجاع آن‌ها را کاهش ندهید!

چند نمونه فراخوانی:

#include <Python.h>
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;

ok = PyArg_ParseTuple(args, ""); /* No arguments */
    /* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
    /* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* دو long و یک رشته */
    /* فراخوانی ممکن پایتون: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* یک جفت عدد صحیح و یک رشته، که اندازه‌ی آن نیز برگردانده می‌شود */
    /* فراخوانی ممکن پایتون: f((1, 2), 'three') */
{
    const char *file;
    const char *mode = "r";
    int bufsize = 0;
    ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
    /* A string, and optionally another string and an integer */
    /* Possible Python calls:
       f('spam')
       f('spam', 'w')
       f('spam', 'wb', 100000) */
}
{
    int left, top, right, bottom, h, v;
    ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
             &left, &top, &right, &bottom, &h, &v);
    /* A rectangle and a point */
    /* Possible Python call:
       f(((0, 0), (400, 300)), (10, 10)) */
}
{
    Py_complex c;
    ok = PyArg_ParseTuple(args, "D:myfunction", &c);
    /* a complex, also providing a function name for errors */
    /* Possible Python call: myfunction(1+2j) */
}

پارامترهای کلیدواژه‌ای برای توابع توسعه‌ای

If you also want your function to accept keyword arguments, use the METH_KEYWORDS flag in combination with METH_VARARGS. (METH_KEYWORDS can also be used with other flags; see its documentation for the allowed combinations.)

In this case, the C function should accept a third PyObject * parameter which will be a dictionary of keywords. Use PyArg_ParseTupleAndKeywords() to parse the arguments to such a function.

تابع PyArg_ParseTupleAndKeywords() به صورت زیر اعلان می‌شود:

int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
                                const char *format, char * const *kwlist, ...);

پارامتر‌های arg و format با پارامتر‌های تابع PyArg_ParseTuple() یکسان هستند. پارامتر kwdict دیکشنری کلیدواژه‌هایی است که به‌عنوان پارامتر سوم از ران‌تایم پایتون دریافت می‌شود. پارامتر kwlist فهرستی از رشته‌هاست که با NULL پایان می‌یابد و پارامترها را شناسایی می‌کند؛ نام‌ها از چپ به راست با اطلاعات نوع گرفته‌شده از format تطبیق داده می‌شوند. در صورت موفقیت، PyArg_ParseTupleAndKeywords() مقدار true را برمی‌گرداند؛ در غیر این صورت مقدار false را برمی‌گرداند و استثنای مناسبی ایجاد می‌کند.

توجه

تاپل‌های تودرتو هنگام استفاده از آرگومان‌های کلیدواژه‌ای قابل تجزیه نیستند! پارامترهای کلیدواژه‌ای که پاس داده می‌شوند و در kwlist وجود ندارند، باعث به‌وجود آمدن TypeError می‌شوند.

در اینجا یک ماژول نمونه که از کلیدواژه‌ها استفاده می‌کند، بر اساس نمونه‌ای از Geoff Philbrick (philbrick@hks.com) آمده است:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    int voltage;
    const char *state = "a stiff";
    const char *action = "voom";
    const char *type = "Norwegian Blue";

    static char *kwlist[] = {"voltage", "state", "action", "type", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
                                     &voltage, &state, &action, &type))
        return NULL;

    printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",
           action, voltage);
    printf("-- Lovely plumage, the %s -- It's %s!\n", type, state);

    Py_RETURN_NONE;
}

static PyMethodDef keywdarg_methods[] = {
    /* The cast of the function is necessary since PyCFunction values
     * only take two PyObject* parameters, and keywdarg_parrot() takes
     * three.
     */
    {"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
     "Print a lovely skit to standard output."},
    {NULL, NULL, 0, NULL}   /* sentinel */
};

ساخت مقادیر دلخواه

این تابع همتای PyArg_ParseTuple() است. این تابع به صورت زیر اعلان می‌شود:

PyObject *Py_BuildValue(const char *format, ...);

این تابع مجموعه‌ای از واحدهای قالب (format units) را تشخیص می‌دهد که مشابه واحدهای شناسایی‌شده توسط PyArg_ParseTuple() هستند، اما آرگومان‌ها (که ورودی تابع هستند، نه خروجی آن) نباید اشاره‌گر باشند، بلکه باید فقط مقدار باشند. این تابع یک شیء جدید پایتون برمی‌گرداند که برای بازگرداندن از یک تابع C فراخوانی‌شده از پایتون مناسب است.

یک تفاوت با PyArg_ParseTuple() این است که در حالی که دومی نیازمند است آرگومان اولش یک تاپل باشد (چرا که فهرست‌های آرگومان پایتون همیشه به صورت داخلی به شکل تاپل نمایش داده می‌شوند)، Py_BuildValue() همیشه یک تاپل نمی‌سازد. این تابع تنها زمانی یک تاپل می‌سازد که رشته قالب‌بندی آن دو واحد قالب‌بندی یا بیشتر داشته باشد. اگر رشته قالب‌بندی خالی باشد، None برمی‌گرداند؛ اگر دقیقاً یک واحد قالب‌بندی داشته باشد، هر شیئی را که آن واحد قالب‌بندی توصیف می‌کند برمی‌گرداند. برای اینکه آن را وادار کنید یک تاپل با اندازه‌ی ۰ یا ۱ برگرداند، رشته قالب‌بندی را داخل پرانتز قرار دهید.

مثال‌ها (در سمت چپ فراخوانی، در سمت راست مقدار حاصل در پایتون):

Py_BuildValue("")                        None
Py_BuildValue("i", 123)                  123
Py_BuildValue("iii", 123, 456, 789)      (123, 456, 789)
Py_BuildValue("s", "hello")              'hello'
Py_BuildValue("y", "hello")              b'hello'
Py_BuildValue("ss", "hello", "world")    ('hello', 'world')
Py_BuildValue("s#", "hello", 4)          'hell'
Py_BuildValue("y#", "hello", 4)          b'hell'
Py_BuildValue("()")                      ()
Py_BuildValue("(i)", 123)                (123,)
Py_BuildValue("(ii)", 123, 456)          (123, 456)
Py_BuildValue("(i,i)", 123, 456)         (123, 456)
Py_BuildValue("[i,i]", 123, 456)         [123, 456]
Py_BuildValue("{s:i,s:i}",
              "abc", 123, "def", 456)    {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
              1, 2, 3, 4, 5, 6)          (((1, 2), (3, 4)), (5, 6))

شمارش ارجاع

در زبان‌هایی مانند C یا C++، برنامه‌نویس مسئول تخصیص پویا و آزادسازی حافظه روی هیپ است. در C، این کار با استفاده از توابع malloc() و free() انجام می‌شود. در C++، عملگرهای new و delete با معنایی اساساً یکسان به کار می‌روند و بحث زیر را به مورد C محدود می‌کنیم.

هر بلوک حافظه‌ای که با malloc() تخصیص داده‌شده باشد، باید در نهایت از طریق دقیقاً یک فراخوانی free() به استخر حافظه‌ی در دسترس بازگردانده شود. مهم است که free() در زمان مناسب فراخوانی شود. اگر نشانی یک بلوک فراموش شود اما free() برای آن فراخوانی نشود، حافظه‌ای که آن بلوک اشغال کرده است، تا زمانی که برنامه خاتمه یابد، قابل استفاده‌ی مجدد نخواهد بود. این وضعیت نشت حافظه (memory leak) نامیده می‌شود. از سوی دیگر، اگر برنامه‌ای free() را برای یک بلوک فراخوانی کند و سپس به استفاده از آن بلوک ادامه دهد، با استفاده‌ی مجدد از آن بلوک از طریق یک فراخوانی دیگر malloc() تداخل ایجاد می‌کند. این کار استفاده از حافظه‌ی آزادشده (using freed memory) نامیده می‌شود. این همان پیامدهای بد ارجاع به داده‌های مقداردهی‌نشده را دارد --- برون‌ریزی هسته، نتایج نادرست، فروپاشی‌های مرموز.

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

از آنجا که پایتون به‌طور گسترده از malloc() و free() استفاده می‌کند، به راهبردی نیاز دارد تا از نشت حافظه و همچنین استفاده از حافظه‌ی آزادشده جلوگیری کند. روش انتخاب‌شده شمارش ارجاع (reference counting) نام دارد. اصل کار ساده است: هر شیء شامل یک شمارنده است که وقتی ارجاعی به شیء در جایی ذخیره می‌شود، افزایش می‌یابد و وقتی ارجاعی به آن حذف می‌شود، کاهش می‌یابد. وقتی شمارنده به صفر برسد، آخرین ارجاع به شیء حذف شده و شیء آزاد می‌شود.

راهبردی جایگزین، زباله‌روبی خودکار (automatic garbage collection) نامیده می‌شود. (گاهی شمارش ارجاع نیز به‌عنوان یک راهبرد زباله‌روبی شناخته می‌شود، از این رو از واژه‌ی «خودکار» برای تمایز میان این دو استفاده می‌شود.) مزیت بزرگ زباله‌روبی خودکار این است که کاربر نیازی به فراخوانی صریح free() ندارد. (مزیت دیگری که ادعا می‌شود، بهبود سرعت یا مصرف حافظه است --- هرچند این موضوع واقعیتی قطعی نیست.) نقطه‌ضعف این است که برای C، هیچ زباله‌روب خودکاری که واقعاً قابل حمل باشد وجود ندارد، در حالی که شمارش ارجاع را می‌توان به‌صورت قابل حمل پیاده‌سازی کرد (تا زمانی که توابع malloc() و free() در دسترس باشند --- که استاندارد C آن را تضمین می‌کند). شاید روزی زباله‌روب خودکاری که به‌اندازه‌ی کافی قابل حمل باشد، برای C در دسترس قرار گیرد. تا آن زمان، چاره‌ای جز سازگاری با شمارش ارجاع نداریم.

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

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

شمارش ارجاع در پایتون

دو ماکرو وجود دارد، Py_INCREF(x) و Py_DECREF(x)، که افزایش و کاهش شمارش ارجاع را بر عهده دارند. Py_DECREF() همچنین هنگامی که شمارش به صفر برسد، شیء را آزاد می‌کند. برای انعطاف‌پذیری، این ماکرو free() را مستقیماً فراخوانی نمی‌کند --- بلکه فراخوانی را از طریق یک اشاره‌گر تابع در شیء نوع (type object) مربوط به آن شیء انجام می‌دهد. برای این منظور (و منظورهای دیگر)، هر شیء همچنین حاوی اشاره‌گری به شیء نوع خود است.

اکنون پرسش بزرگ این است: چه زمانی باید از Py_INCREF(x) و Py_DECREF(x) استفاده کرد؟ اجازه دهید ابتدا چند اصطلاح را معرفی کنیم. هیچ‌کس «مالک» یک شیء نیست؛ اما شما می‌توانید مالک یک ارجاع (own a reference) به یک شیء باشید. اکنون شمارش ارجاع یک شیء به‌عنوان تعداد ارجاع‌های در مالکیت به آن تعریف می‌شود. مالک یک ارجاع مسئول است که وقتی دیگر به ارجاع نیازی نیست، Py_DECREF() را فراخوانی کند. مالکیت یک ارجاع می‌تواند منتقل شود. سه راه برای خلاص شدن از یک ارجاع در مالکیت وجود دارد: انتقال آن، ذخیره‌ی آن، یا فراخوانی Py_DECREF(). فراموش کردن خلاص شدن از یک ارجاع در مالکیت، باعث نشت حافظه می‌شود.

It is also possible to borrow [1] a reference to an object. The borrower of a reference should not call Py_DECREF(). The borrower must not hold on to the object longer than the owner from which it was borrowed. Using a borrowed reference after the owner has disposed of it risks using freed memory and should be avoided completely [2].

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

یک ارجاع امانتی را می‌توان با فراخوانی Py_INCREF() به یک ارجاع ملکی تبدیل کرد. این کار تأثیری بر وضعیت مالکی که ارجاع از او امانت گرفته‌شده بود نمی‌گذارد --- بلکه یک ارجاع ملکی جدید ایجاد می‌کند و مسئولیت‌های کامل مالکیت را به همراه دارد (مالک جدید باید ارجاع را به‌درستی دفع کند، همان‌طور که مالک قبلی نیز باید این کار را انجام دهد).

قواعد مالکیت

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

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

بسیاری از توابعی که اشیاء را از اشیاء دیگر استخراج می‌کنند، مالکیت را نیز همراه با ارجاع منتقل می‌کنند؛ برای مثال PyObject_GetAttrString(). با این حال، تصویر در اینجا کمتر روشن است، زیرا چند روال رایج استثنا هستند: PyTuple_GetItem()، PyList_GetItem()، PyDict_GetItem() و PyDict_GetItemString() همگی ارجاع‌هایی را برمی‌گردانند که شما آن‌ها را از تاپل، فهرست یا دیکشنری امانت می‌گیرید.

تابع PyImport_AddModule() نیز یک ارجاع امانتی برمی‌گرداند، هرچند ممکن است در واقع شیئی را که برمی‌گرداند ایجاد کند: این امر ممکن است زیرا یک ارجاع مالکانه به شیء در sys.modules ذخیره شده است.

وقتی یک ارجاع به شیء را به تابع دیگری می‌دهید، به‌طور کلی، تابع ارجاع را به‌صورت امانتی از شما می‌گیرد --- اگر نیاز به ذخیره‌سازی آن داشته باشد، از Py_INCREF() استفاده می‌کند تا مالکی مستقل شود. دقیقاً دو استثنای مهم برای این قاعده وجود دارد: PyTuple_SetItem() و PyList_SetItem(). این توابع مالکیت آیتمی را که به آن‌ها داده می‌شود در اختیار می‌گیرند --- حتی اگر شکست بخورند! (توجه کنید که PyDict_SetItem() و توابع مشابه مالکیت را در اختیار نمی‌گیرند --- آن‌ها «معمولی» هستند.)

وقتی یک تابع C از پایتون فراخوانی می‌شود، ارجاع‌هایی به آرگومان‌های خود را از فراخوانی‌کننده به امانت می‌گیرد. فراخوانی‌کننده مالک ارجاعی به شیء است، بنابراین طول عمر ارجاع امانتی تا زمان بازگشت تابع تضمین‌شده است. تنها زمانی که چنین ارجاع امانتی‌ای باید ذخیره یا منتقل شود، باید با فراخوانی Py_INCREF() به یک ارجاع مالکانه (owned reference) تبدیل شود.

ارجاع شیئی که از یک تابع C فراخوانی‌شده از پایتون بازگردانده می‌شود، باید یک ارجاع مالکانه (owned reference) باشد --- مالکیت از تابع به فراخواننده‌ی آن منتقل می‌شود.

یخ نازک

چند موقعیت وجود دارد که استفاده‌ی به‌ظاهر بی‌ضرر از یک ارجاع امانتی می‌تواند منجر به مشکلات شود. همه‌ی این موارد به فراخوانی‌های ضمنی مفسر مربوط‌اند که می‌توانند باعث شوند مالک یک ارجاع آن را رها کند.

نخستین و مهم‌ترین موردی که باید از آن آگاه باشید، استفاده از Py_DECREF() روی شیئی نامرتبط در حالی است که ارجاعی به یک آیتم فهرست را به امانت گرفته‌اید. برای نمونه:

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0); /* BUG! */
}

این تابع ابتدا یک ارجاع امانتی به list[0] می‌گیرد، سپس list[1] را با مقدار 0 جایگزین می‌کند و در نهایت ارجاع امانتی را چاپ می‌کند. بی‌ضرر به نظر می‌رسد، نه؟ اما چنین نیست!

بیایید جریان کنترل را تا داخل PyList_SetItem() دنبال کنیم. فهرست مالک ارجاع‌هایی به همه‌ی آیتم‌های خود است، بنابراین وقتی آیتم ۱ جایگزین می‌شود، فهرست باید از آیتم اصلی ۱ خلاص شود. اکنون فرض کنید آیتم اصلی ۱ نمونه‌ای از یک کلاس تعریف‌شده توسط کاربر بوده است، و همچنین فرض کنید این کلاس متد __del__() را تعریف کرده است. اگر شمارش ارجاع این نمونه‌ی کلاس برابر با ۱ باشد، خلاص شدن از آن، متد __del__() آن را فراخوانی می‌کند. به‌طور داخلی، PyList_SetItem() Py_DECREF() را روی آیتم جایگزین‌شده فراخوانی می‌کند، که این امر موجب فراخوانی تابع tp_dealloc متناظر با آیتم جایگزین‌شده می‌شود. در حین تخصیص‌زدایی، tp_dealloc tp_finalize را فراخوانی می‌کند که برای نمونه‌های کلاس به متد __del__() نگاشت شده است (به PEP 442 مراجعه کنید). کل این دنباله به‌صورت همگام درون فراخوانی PyList_SetItem() رخ می‌دهد.

از آنجا که به زبان پایتون نوشته شده است، متد __del__() می‌تواند کد پایتون دلخواهی را اجرا کند. آیا ممکن است کاری انجام دهد که ارجاع به item در bug() را بی‌اعتبار کند؟ قطعاً! با فرض اینکه فهرست پاس‌داده‌شده به bug() در دسترس متد __del__() باشد، می‌تواند دستوری مانند del list[0] اجرا کند، و با فرض اینکه این آخرین ارجاع به آن شیء باشد، حافظه‌ی مرتبط با آن را آزاد می‌کند و بدین ترتیب item را بی‌اعتبار می‌کند.

راه‌حل، وقتی منشأ مشکل را بدانید، آسان است: شمارش ارجاع را به‌طور موقت افزایش دهید. نسخه صحیح تابع چنین است:

void
no_bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    Py_INCREF(item);
    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0);
    Py_DECREF(item);
}

این داستانی واقعی است. نسخه‌ای قدیمی‌تر از پایتون شامل گونه‌هایی از این باگ بود و کسی مدت زمان قابل توجهی را در دیباگر C صرف کرد تا بفهمد چرا متدهای __del__() او شکست می‌خوردند...

دومین مورد از مشکلات مربوط به ارجاع امانتی، گونه‌ای است که به نخ‌ها مربوط می‌شود. به‌طور معمول، نخ‌های متعدد در مفسر پایتون نمی‌توانند مزاحم کار یکدیگر شوند، زیرا یک قفل سراسری از کل فضای اشیای پایتون محافظت می‌کند. با این حال، می‌توان این قفل را به‌طور موقت با استفاده از ماکروی Py_BEGIN_ALLOW_THREADS آزاد کرد و آن را با استفاده از Py_END_ALLOW_THREADS دوباره به دست آورد. این کار پیرامون فراخوانی‌های مسدودکننده‌ی ورودی/خروجی رایج است تا نخ‌های دیگر بتوانند در حین انتظار برای تکمیل ورودی/خروجی، از پردازنده استفاده کنند. بدیهی است که تابع زیر همان مشکلی را دارد که تابع قبلی داشت:

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);
    Py_BEGIN_ALLOW_THREADS
    ...some blocking I/O call...
    Py_END_ALLOW_THREADS
    PyObject_Print(item, stdout, 0); /* BUG! */
}

اشاره‌گرهای NULL

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

بهتر است بررسی NULL تنها در "source:" انجام شود؛ زمانی که اشاره‌گری که ممکن است NULL باشد دریافت می‌شود، برای مثال از malloc() یا از تابعی که ممکن است استثنا برافراخواند.

ماکروهای Py_INCREF() و Py_DECREF() اشاره‌گرهای NULL را بررسی نمی‌کنند --- اما گونه‌های آن‌ها، Py_XINCREF() و Py_XDECREF()، این بررسی را انجام می‌دهند.

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

The C function calling mechanism guarantees that the argument list passed to C functions (args in the examples) is never NULL --- in fact it guarantees that it is always a tuple [3].

این خطایی بسیار جدی است که اشاره‌گر NULL به کاربر پایتون «گریز» کند.

نوشتن توسعه‌ها با C++

نوشتن ماژول‌های توسعه‌ای به زبان C++ ممکن است. برخی محدودیت‌ها اعمال می‌شوند. اگر برنامه اصلی (مفسر پایتون) توسط کامپایلر C کامپایل و پیوند داده شود، نمی‌توان از اشیاء سراسری یا ایستای دارای سازنده استفاده کرد. اگر برنامه اصلی توسط کامپایلر C++ پیوند داده شود، این مشکل وجود ندارد. توابعی که توسط مفسر پایتون فراخوانی خواهند شد (به‌ویژه توابع مقداردهی اولیه ماژول) باید با استفاده از extern "C" اعلام شوند. لازم نیست پرونده‌های سرآیند پایتون در extern "C" {...} محصور شوند --- این پرونده‌ها در صورتی که نماد __cplusplus تعریف شده باشد، از قبل به این شکل هستند (همه‌ی کامپایلرهای جدید C++ این نماد را تعریف می‌کنند).

ارائه‌ی یک C API برای ماژول توسعه‌ای

بسیاری از ماژول‌های توسعه‌ای صرفاً توابع و نوع‌های جدیدی فراهم می‌کنند که از پایتون مورد استفاده قرار می‌گیرند، اما گاهی کد موجود در یک ماژول توسعه‌ای می‌تواند برای ماژول‌های توسعه‌ای دیگر مفید باشد. برای مثال، یک ماژول توسعه‌ای می‌تواند نوعی به نام «مجموعه» پیاده‌سازی کند که مانند فهرست‌های بدون ترتیب کار می‌کند. درست همان‌طور که نوع فهرست استاندارد پایتون دارای یک C API است که به ماژول‌های توسعه‌ای اجازه می‌دهد فهرست‌ها را ایجاد و دستکاری کنند، این نوع مجموعه جدید نیز باید مجموعه‌ای از توابع C برای دستکاری مستقیم از سوی ماژول‌های توسعه‌ای دیگر داشته باشد.

در نگاه نخست این کار آسان به نظر می‌رسد: کافی است توابع را بنویسید (البته بدون اینکه آن‌ها را static اعلان کنید)، یک پرونده‌ی سرآیند مناسب فراهم کنید و API زبان C را مستند کنید. و در واقع اگر همه ماژول‌های توسعه‌ای همیشه به‌صورت ایستا با مفسر پایتون پیوند داده می‌شدند، همین روش کار می‌کرد. اما هنگامی که ماژول‌ها به‌صورت کتابخانه‌های اشتراکی استفاده می‌شوند، ممکن است نمادهای تعریف‌شده در یک ماژول برای ماژول دیگری نمایان نباشند. جزئیات نمایان بودن نمادها به سیستم‌عامل بستگی دارد؛ برخی سیستم‌ها برای مفسر پایتون و همه ماژول‌های توسعه‌ای از یک فضای نام سراسری واحد استفاده می‌کنند (مثلاً ویندوز)، در حالی که برخی دیگر در زمان پیوند ماژول به فهرست صریحی از نمادهای واردشده نیاز دارند (AIX یکی از این نمونه‌هاست)، یا امکان انتخاب میان راهبردهای مختلف را ارائه می‌دهند (بیشتر سیستم‌های یونیکس). و حتی اگر نمادها به‌صورت سراسری نمایان باشند، ممکن است ماژولی که می‌خواهید توابعش را فراخوانی کنید، هنوز بارگذاری‌نشده باشد!

Portability therefore requires not to make any assumptions about symbol visibility. This means that all symbols in extension modules should be declared static, except for the module's initialization function, in order to avoid name clashes with other extension modules. And it means that symbols that should be accessible from other extension modules must be exported in a different way.

پایتون سازوکار ویژه‌ای برای انتقال اطلاعات در سطح C (اشاره‌گرها) از یک ماژول توسعه‌ای به ماژول توسعه‌ای دیگر فراهم می‌کند: کپسول‌ها (Capsules). کپسول یک نوع داده پایتونی است که یک اشاره‌گر (void*) را ذخیره می‌کند. ایجاد و دسترسی به کپسول‌ها تنها از طریق API زبان C آن‌ها ممکن است، اما می‌توان آن‌ها را مانند هر شیء پایتونی دیگری منتقل کرد. به‌طور خاص، می‌توان آن‌ها را به نامی در فضای نام یک ماژول توسعه‌ای منتسب کرد. سپس ماژول‌های توسعه‌ای دیگر می‌توانند این ماژول را ایمپورت کنند، مقدار این نام را بازیابی کنند و سپس اشاره‌گر را از کپسول بازیابی کنند.

از کپسول‌ها می‌توان به روش‌های بسیاری برای اکسپورت کردن API زبان C یک ماژول توسعه‌ای استفاده کرد. هر تابع می‌تواند کپسول مخصوص خود را داشته باشد، یا تمام اشاره‌گرهای API زبان C می‌توانند در آرایه‌ای ذخیره شوند که آدرس آن در یک کپسول منتشر می‌شود. همچنین می‌توان وظایف گوناگون ذخیره و بازیابی اشاره‌گرها را به شیوه‌های مختلف بین ماژولی که کد را فراهم می‌کند و ماژول‌های مشتری توزیع کرد.

هر روشی را که انتخاب کنید، مهم است که کپسول‌های خود را به‌درستی نام‌گذاری کنید. تابع PyCapsule_New() یک پارامتر نام می‌گیرد (const char*)؛ شما مجازید نام NULL ارسال کنید، اما ما به‌شدت توصیه می‌کنیم که نامی مشخص کنید. کپسول‌های به‌درستی نام‌گذاری‌شده درجه‌ای از ایمنی نوع در زمان اجرا فراهم می‌کنند؛ هیچ راه عملی‌ای برای تمایز یک کپسول بدون نام از کپسول دیگر وجود ندارد.

به‌طور خاص، کپسول‌هایی که برای افشای APIهای C استفاده می‌شوند، باید مطابق این قرارداد نام‌گذاری شوند:

modulename.attributename

تابع کمکی PyCapsule_Import() بارگذاری یک C API که از طریق یک کپسول ارائه شده را آسان می‌کند، اما تنها در صورتی که نام کپسول با این قرارداد مطابقت داشته باشد. این رفتار به کاربران C API درجه بالایی از اطمینان می‌دهد که کپسولی که بارگذاری می‌کنند، C API صحیح را در بر دارد.

مثال زیر رویکردی را نشان می‌دهد که بیشتر بار را بر دوش نویسنده ماژول اکسپورتکننده قرار می‌دهد، که برای ماژول‌های کتابخانه‌ای پرکاربرد مناسب است. این رویکرد تمام اشاره‌گرهای C API (در مثال فقط یکی!) را در آرایه‌ای از اشاره‌گرهای void ذخیره می‌کند که مقدار یک کپسول می‌شود. پرونده‌ی سرآیند متناظر با ماژول، ماکرویی را فراهم می‌کند که کار ایمپورت کردن ماژول و بازیابی اشاره‌گرهای C API آن را انجام می‌دهد؛ ماژول‌های کلاینت تنها باید پیش از دسترسی به C API این ماکرو را فراخوانی کنند.

The exporting module is a modification of the spam module from the tutorial. The function spam.system() does not call the C library function system() directly, but a function PySpam_System(), which would of course do something more complicated in reality (such as adding "spam" to every command). This function PySpam_System() is also exported to other extension modules.

تابع PySpam_System() یک تابع C ساده است که مانند بقیه موارد به‌صورت static اعلان شده است:

static int
PySpam_System(const char *command)
{
    return system(command);
}

تابع spam_system() به شکلی ساده تغییر داده می‌شود:

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = PySpam_System(command);
    return PyLong_FromLong(sts);
}

در ابتدای ماژول، درست پس از سطر

#include <Python.h>

دو سطر دیگر باید افزوده شود:

#define SPAM_MODULE
#include "spammodule.h"

از #define استفاده می‌شود تا به پرونده‌ی سرآیند بگوید که در ماژول اکسپورتکننده گنجانده می‌شود، نه در یک ماژول کلاینت. در نهایت، تابع mod_exec ماژول باید مقداردهی اولیه‌ی آرایه اشاره‌گرهای C API را بر عهده بگیرد:

static int
spam_module_exec(PyObject *m)
{
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    /* Initialize the C API pointer array */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* Create a Capsule containing the API pointer array's address */
    c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);

    if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
        return -1;
    }

    return 0;
}

توجه داشته باشید که PySpam_API به‌صورت static تعریف شده است؛ در غیر این صورت، آرایه اشاره‌گرها با خاتمه‌یافتن PyInit_spam() ناپدید می‌شد!

بخش عمده‌ی کار در پرونده‌ی سرآیند spammodule.h قرار دارد که به این شکل است:

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif

/* Header file for spammodule */

/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)

/* Total number of C API pointers */
#define PySpam_API_pointers 1


#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* This section is used in modules that use spammodule's API */

static void **PySpam_API;

#define PySpam_System \
 (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])

/* Return -1 on error, 0 on success.
 * PyCapsule_Import will set an exception if there's an error.
 */
static int
import_spam(void)
{
    PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
    return (PySpam_API != NULL) ? 0 : -1;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H) */

تنها کاری که یک ماژول کلاینت باید انجام دهد تا به تابع PySpam_System() دسترسی داشته باشد، این است که تابع (یا بهتر بگوییم، ماکرو) import_spam() را در تابع mod_exec خود فراخوانی کند:

static int
client_module_exec(PyObject *m)
{
    if (import_spam() < 0) {
        return -1;
    }
    /* additional initialization can happen here */
    return 0;
}

عیب اصلی این رویکرد این است که پرونده‌ی spammodule.h نسبتاً پیچیده است. با این حال، ساختار پایه برای هر تابعی که اکسپورت می‌شود یکسان است، بنابراین تنها یک بار باید یاد گرفته شود.

در نهایت باید ذکر شود که Capsule‌ها عملکردهای اضافی ارائه می‌دهند، که به‌ویژه برای تخصیص و آزادسازی حافظه اشاره‌گر ذخیره‌شده در یک Capsule مفید هستند. جزئیات در راهنمای ارجاع Python/C API در بخش کپسول‌ها (Capsules) و در پیاده‌سازی Capsuleها (پرونده‌های Include/pycapsule.h و Objects/capsule.c در توزیع کد منبع پایتون) توصیف شده‌اند.

پانوشت‌ها