1. Mở rộng Python bằng C hoặc C++

Việc thêm các mô-đun tích hợp mới vào Python khá dễ dàng nếu bạn biết cách lập trình trong C. extension modules như vậy có thể thực hiện hai việc mà không thể thực hiện trực tiếp trong Python: chúng có thể triển khai các loại đối tượng tích hợp mới và chúng có thể gọi các hàm thư viện C và lệnh gọi hệ thống.

Để hỗ trợ các tiện ích mở rộng, Python API (Giao diện lập trình ứng dụng) xác định một tập hợp các hàm, macro và biến cung cấp quyền truy cập vào hầu hết các khía cạnh của hệ thống thời gian chạy Python. Python API được kết hợp trong tệp nguồn C bằng cách bao gồm tiêu đề "Python.h".

Việc biên soạn mô-đun mở rộng phụ thuộc vào mục đích sử dụng cũng như thiết lập hệ thống của bạn; chi tiết được đưa ra trong các chương sau.

Ghi chú

Giao diện mở rộng C dành riêng cho CPython và các mô-đun mở rộng không hoạt động trên các triển khai Python khác. Trong nhiều trường hợp, có thể tránh viết các phần mở rộng C và duy trì tính di động cho các triển khai khác. Ví dụ: nếu trường hợp sử dụng của bạn đang gọi các hàm thư viện C hoặc lệnh gọi hệ thống, bạn nên cân nhắc sử dụng mô-đun ctypes hoặc thư viện cffi thay vì viết mã C tùy chỉnh. Các mô-đun này cho phép bạn viết mã Python để giao tiếp với mã C và dễ di chuyển hơn giữa các lần triển khai Python so với việc viết và biên dịch mô-đun mở rộng C.

1.1. Một ví dụ đơn giản

Hãy tạo một mô-đun mở rộng có tên spam (món ăn yêu thích của những người hâm mộ Monty Python...) và giả sử chúng ta muốn tạo giao diện Python cho hàm thư viện C system() [1]. Hàm này lấy chuỗi ký tự kết thúc bằng null làm đối số và trả về một số nguyên. Chúng tôi muốn hàm này có thể gọi được từ Python như sau:

>>> nhập thư rác
>>> trạng thái = spam.system("ls -l")

Bắt đầu bằng cách tạo một tệp spammodule.c. (Trước đây, nếu một mô-đun có tên là spam thì tệp C chứa phần triển khai của nó được gọi là spammodule.c; nếu tên mô-đun rất dài, như spammify, thì tên mô-đun có thể chỉ là spammify.c.)

Hai dòng đầu tiên của tệp của chúng tôi có thể là:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

kéo theo Python API (bạn có thể thêm nhận xét mô tả mục đích của mô-đun và thông báo bản quyền nếu muốn).

Ghi chú

Vì Python có thể xác định một số định nghĩa tiền xử lý ảnh hưởng đến các tiêu đề tiêu chuẩn trên một số hệ thống, nên bạn must bao gồm Python.h trước bất kỳ tiêu đề tiêu chuẩn nào được đưa vào.

#define PY_SSIZE_T_CLEAN được dùng để chỉ ra rằng nên sử dụng Py_ssize_t trong một số API thay vì int. Nó không cần thiết kể từ Python 3.13, nhưng chúng tôi giữ nó ở đây để tương thích ngược. Xem Chuỗi và bộ đệm để biết mô tả về macro này.

Tất cả các ký hiệu mà người dùng nhìn thấy được xác định bởi Python.h đều có tiền tố là Py hoặc PY, ngoại trừ những ký hiệu được xác định trong tệp tiêu đề tiêu chuẩn.

Mẹo

Để tương thích ngược, Python.h bao gồm một số tệp tiêu đề tiêu chuẩn. Tiện ích mở rộng C phải bao gồm các tiêu đề tiêu chuẩn mà chúng sử dụng và không nên dựa vào các tiêu đề ngầm này. Nếu sử dụng phiên bản C API giới hạn 3.13 trở lên thì hàm ý bao gồm:

  • <assert.h>

  • <intrin.h> (trên Windows)

  • <inttypes.h>

  • <limits.h>

  • <math.h>

  • <stdarg.h>

  • <wchar.h>

  • <sys/types.h> (nếu có)

Nếu Py_LIMITED_API không được xác định hoặc được đặt ở phiên bản 3.12 trở lên thì các tiêu đề bên dưới cũng được bao gồm:

  • <ctype.h>

  • <unistd.h> (trên POSIX)

Nếu Py_LIMITED_API không được xác định hoặc được đặt ở phiên bản 3.10 trở lên thì các tiêu đề bên dưới cũng được bao gồm:

  • <errno.h>

  • <stdio.h>

  • <stdlib.h>

  • <string.h>

Điều tiếp theo mà chúng tôi thêm vào tệp mô-đun của mình là hàm C sẽ được gọi khi biểu thức Python spam.system(string) được đánh giá (chúng tôi sẽ xem ngay cách nó được gọi):

PyObject tĩnh *
spam_system(PyObject *self, PyObject *args)
{
    lệnh const char *;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        trả lại NULL;
    sts = hệ thống(lệnh);
    trả về PyLong_FromLong(sts);
}

Có một bản dịch đơn giản từ danh sách đối số trong Python (ví dụ: biểu thức đơn "ls -l") sang các đối số được truyền cho hàm C. Hàm C luôn có hai đối số, thường được đặt tên là selfargs.

Đối số self trỏ đến đối tượng mô-đun cho các hàm cấp mô-đun; đối với một phương thức, nó sẽ trỏ đến thể hiện đối tượng.

Đối số args sẽ là một con trỏ tới đối tượng bộ dữ liệu Python chứa các đối số. Mỗi mục của bộ dữ liệu tương ứng với một đối số trong danh sách đối số của lệnh gọi. Các đối số là các đối tượng Python --- để thực hiện bất kỳ điều gì với chúng trong hàm C, chúng ta phải chuyển đổi chúng thành giá trị C. Hàm PyArg_ParseTuple() trong Python API kiểm tra các loại đối số và chuyển đổi chúng thành giá trị C. Nó sử dụng một chuỗi mẫu để xác định các loại đối số cần thiết cũng như các loại biến C để lưu trữ các giá trị được chuyển đổi. Thông tin thêm về điều này sau.

PyArg_ParseTuple() trả về true (khác 0) nếu tất cả đối số có đúng loại và các thành phần của nó đã được lưu trữ trong các biến có địa chỉ được truyền. Nó trả về false (không) nếu danh sách đối số không hợp lệ được thông qua. Trong trường hợp sau, nó cũng đưa ra một ngoại lệ thích hợp để hàm gọi có thể trả về NULL ngay lập tức (như chúng ta đã thấy trong ví dụ).

1.2. Intermezzo: Lỗi và ngoại lệ

Một quy ước quan trọng xuyên suốt trình thông dịch Python là: khi một hàm bị lỗi, nó phải đặt một điều kiện ngoại lệ và trả về một giá trị lỗi (thường là -1 hoặc con trỏ NULL). Thông tin ngoại lệ được lưu trữ trong ba thành viên của trạng thái luồng của trình thông dịch. Đây là NULL nếu không có ngoại lệ. Mặt khác, chúng là các thành phần tương đương C của các thành viên trong bộ Python được trả về bởi sys.exc_info(). Đây là loại ngoại lệ, trường hợp ngoại lệ và đối tượng truy nguyên. Điều quan trọng là phải biết về chúng để hiểu lỗi được truyền đi như thế nào.

Python API xác định một số hàm để đặt nhiều loại ngoại lệ khác nhau.

Phổ biến nhất là PyErr_SetString(). Đối số của nó là một đối tượng ngoại lệ và một chuỗi C. Đối tượng ngoại lệ thường là đối tượng được xác định trước như PyExc_ZeroDivisionError. Chuỗi C cho biết nguyên nhân gây ra lỗi và được chuyển đổi thành đối tượng chuỗi Python và được lưu dưới dạng "giá trị liên quan" của ngoại lệ.

Một hàm hữu ích khác là PyErr_SetFromErrno(), hàm này chỉ nhận một đối số ngoại lệ và xây dựng giá trị liên quan bằng cách kiểm tra biến toàn cục errno. Hàm tổng quát nhất là PyErr_SetObject(), hàm này nhận hai đối số đối tượng, ngoại lệ và giá trị liên quan của nó. Bạn không cần phải Py_INCREF() các đối tượng được truyền cho bất kỳ hàm nào trong số này.

Bạn có thể kiểm tra không phá hủy xem ngoại lệ có được đặt bằng PyErr_Occurred() hay không. Điều này trả về đối tượng ngoại lệ hiện tại hoặc NULL nếu không có ngoại lệ nào xảy ra. Thông thường, bạn không cần gọi PyErr_Occurred() để xem liệu có xảy ra lỗi trong lệnh gọi hàm hay không vì bạn có thể biết từ giá trị trả về.

Khi một hàm f gọi một hàm khác g phát hiện ra rằng hàm sau bị lỗi, thì f sẽ tự trả về một giá trị lỗi (thường là NULL hoặc -1). not sẽ gọi một trong các hàm PyErr_* --- một hàm đã được g gọi. Sau đó, người gọi f cũng phải trả về một chỉ báo lỗi cho người gọi its, một lần nữa without gọi PyErr_*, v.v. --- nguyên nhân chi tiết nhất của lỗi đã được chức năng phát hiện lỗi đầu tiên báo cáo. Khi lỗi chạm tới vòng lặp chính của trình thông dịch Python, thao tác này sẽ hủy bỏ mã Python hiện đang thực thi và cố gắng tìm một trình xử lý ngoại lệ do lập trình viên Python chỉ định.

(Có những tình huống trong đó mô-đun thực sự có thể đưa ra thông báo lỗi chi tiết hơn bằng cách gọi một hàm PyErr_* khác và trong những trường hợp như vậy, bạn có thể làm như vậy. Tuy nhiên, theo nguyên tắc chung, điều này là không cần thiết và có thể khiến thông tin về nguyên nhân lỗi bị mất: hầu hết các thao tác có thể không thành công vì nhiều lý do.)

Để bỏ qua một ngoại lệ được đặt bởi lệnh gọi hàm không thành công, điều kiện ngoại lệ phải được xóa rõ ràng bằng cách gọi PyErr_Clear(). Lần duy nhất mã C nên gọi PyErr_Clear() là nếu nó không muốn chuyển lỗi cho trình thông dịch mà muốn tự xử lý nó hoàn toàn (có thể bằng cách thử cách khác hoặc giả vờ như không có gì sai).

Mọi cuộc gọi malloc() không thành công phải được chuyển thành một ngoại lệ --- người gọi trực tiếp malloc() (hoặc realloc()) phải gọi PyErr_NoMemory() và trả về chính chỉ báo lỗi. Tất cả các hàm tạo đối tượng (ví dụ: PyLong_FromLong()) đều đã thực hiện việc này, vì vậy lưu ý này chỉ liên quan đến những người gọi trực tiếp malloc().

Cũng lưu ý rằng, ngoại trừ PyArg_ParseTuple() và những người bạn, các hàm trả về trạng thái số nguyên thường trả về giá trị dương hoặc 0 nếu thành công và -1 nếu thất bại, chẳng hạn như các lệnh gọi hệ thống Unix.

Cuối cùng, hãy cẩn thận dọn sạch rác (bằng cách thực hiện lệnh gọi Py_XDECREF() hoặc Py_DECREF() cho các đối tượng bạn đã tạo) khi bạn trả về một chỉ báo lỗi!

Việc lựa chọn ngoại lệ nào để nêu ra hoàn toàn là của bạn. Có các đối tượng C được khai báo trước tương ứng với tất cả các ngoại lệ Python tích hợp, chẳng hạn như PyExc_ZeroDivisionError, mà bạn có thể sử dụng trực tiếp. Tất nhiên, bạn nên chọn ngoại lệ một cách khôn ngoan --- không sử dụng PyExc_TypeError để có nghĩa là không thể mở được tệp (có thể là PyExc_OSError). Nếu có gì đó không ổn với danh sách đối số, hàm PyArg_ParseTuple() thường tăng PyExc_TypeError. Nếu bạn có một đối số có giá trị phải nằm trong một phạm vi cụ thể hoặc phải đáp ứng các điều kiện khác thì PyExc_ValueError là phù hợp.

Bạn cũng có thể xác định một ngoại lệ mới dành riêng cho mô-đun của mình. Cách đơn giản nhất để làm điều này là khai báo một biến đối tượng toàn cục tĩnh ở đầu tệp

PyObject tĩnh *SpamError = NULL;

và khởi tạo nó bằng cách gọi PyErr_NewException() trong hàm Py_mod_exec của mô-đun (spam_module_exec()):

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

SpamError là biến toàn cục nên nó sẽ bị ghi đè mỗi khi mô-đun được khởi tạo lại khi hàm Py_mod_exec được gọi.

Hiện tại, hãy tránh vấn đề này: chúng tôi sẽ chặn việc khởi tạo lặp lại bằng cách tăng ImportError:

PyObject tĩnh *SpamError = NULL;

int tĩnh
spam_module_exec(PyObject *m)
{
    if (SpamError != NULL) {
        PyErr_SetString(PyExc_ImportError,
                        "không thể khởi tạo mô-đun thư rác nhiều lần");
        trả về -1;
    }
    SpamError = PyErr_NewException("spam.error", NULL, NULL);
    if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) {
        trả về -1;
    }

    trả về 0;
}

tĩnh PyModuleDef_Slot spam_module_slots[] = {
    {Py_mod_exec, spam_module_exec},
    {0, NULL}
};

cấu trúc tĩnh PyModuleDef spam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "thư rác",
    .m_size = 0, // không âm
    .m_slots = spam_module_slots,
};

PyMODINIT_FUNC
PyInit_spam(void)
{
    trả về PyModuleDef_Init(&spam_module);
}

Lưu ý rằng tên Python cho đối tượng ngoại lệ là spam.error. Hàm PyErr_NewException() có thể tạo một lớp với lớp cơ sở là Exception (trừ khi một lớp khác được truyền vào thay vì NULL), được mô tả trong Ngoại lệ tích hợp.

Cũng lưu ý rằng biến SpamError giữ lại tham chiếu đến lớp ngoại lệ mới được tạo; đây là cố ý! Vì ngoại lệ có thể được loại bỏ khỏi mô-đun bằng mã bên ngoài nên cần có một tham chiếu thuộc sở hữu của lớp để đảm bảo rằng nó sẽ không bị loại bỏ, khiến SpamError trở thành một con trỏ lơ lửng. Nếu nó trở thành một con trỏ lơ lửng, mã C đưa ra ngoại lệ có thể gây ra kết xuất lõi hoặc các tác dụng phụ ngoài ý muốn khác.

Hiện tại, lệnh gọi Py_DECREF() để xóa tham chiếu này bị thiếu. Ngay cả khi trình thông dịch Python tắt, biến SpamError toàn cục sẽ không được thu thập rác. Nó sẽ "rò rỉ". Tuy nhiên, chúng tôi đã đảm bảo rằng điều này sẽ xảy ra nhiều nhất một lần trong mỗi quy trình.

Chúng ta sẽ thảo luận về việc sử dụng PyMODINIT_FUNC làm kiểu trả về hàm ở phần sau trong mẫu này.

Ngoại lệ spam.error có thể được đưa ra trong mô-đun tiện ích mở rộng của bạn bằng cách gọi tới PyErr_SetString() như hiển thị bên dưới

PyObject tĩnh *
spam_system(PyObject *self, PyObject *args)
{
    lệnh const char *;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        trả lại NULL;
    sts = hệ thống(lệnh);
    nếu (st < 0) {
        PyErr_SetString(SpamError, "Lệnh hệ thống không thành công");
        trả lại NULL;
    }
    trả về PyLong_FromLong(sts);
}

1.3. Quay lại ví dụ

Quay trở lại hàm ví dụ của chúng tôi, bây giờ bạn có thể hiểu được câu lệnh này:

if (!PyArg_ParseTuple(args, "s", &command))
    trả về NULL;

Nó trả về NULL (chỉ báo lỗi cho các hàm trả về con trỏ đối tượng) nếu phát hiện thấy lỗi trong danh sách đối số, dựa vào ngoại lệ do PyArg_ParseTuple() đặt. Nếu không, giá trị chuỗi của đối số đã được sao chép sang biến cục bộ command. Đây là phép gán con trỏ và bạn không được phép sửa đổi chuỗi mà nó trỏ tới (vì vậy, trong Tiêu chuẩn C, biến command phải được khai báo chính xác là const char *command).

Câu lệnh tiếp theo là lệnh gọi hàm Unix system(), truyền cho nó chuỗi chuỗi chúng ta vừa nhận được từ PyArg_ParseTuple():

sts = hệ thống(lệnh);

Hàm spam.system() của chúng tôi phải trả về giá trị của sts dưới dạng đối tượng Python. Điều này được thực hiện bằng cách sử dụng hàm PyLong_FromLong().

trả về PyLong_FromLong(sts);

Trong trường hợp này, nó sẽ trả về một đối tượng số nguyên. (Có, ngay cả số nguyên cũng là đối tượng trên vùng heap trong Python!)

Nếu bạn có hàm C không trả về đối số hữu ích (hàm trả về void), thì hàm Python tương ứng phải trả về None. Bạn cần thành ngữ này để làm như vậy (được triển khai bởi macro Py_RETURN_NONE):

Py_INCREF(Py_None);
trả về Py_None;

Py_None là tên C của đối tượng Python đặc biệt None. Nó là một đối tượng Python chính hãng chứ không phải là con trỏ NULL, có nghĩa là "lỗi" trong hầu hết các ngữ cảnh, như chúng ta đã thấy.

1.4. Bảng phương thức và hàm khởi tạo của mô-đun

Tôi đã hứa sẽ chỉ ra cách gọi spam_system() từ các chương trình Python. Đầu tiên, chúng ta cần liệt kê tên và địa chỉ của nó trong "bảng phương thức":

tĩnh PyMethodDef spam_methods[] = {
    ...
    {"hệ thống", spam_system, METH_VARARGS,
     "Thực thi lệnh shell."},
    ...
    {NULL, NULL, 0, NULL} /* Trọng điểm */
};

Lưu ý mục thứ ba (METH_VARARGS). Đây là cờ báo cho trình thông dịch biết quy ước gọi sẽ được sử dụng cho hàm C. Thông thường nó phải luôn là METH_VARARGS hoặc METH_VARARGS | METH_KEYWORDS; giá trị 0 có nghĩa là một biến thể lỗi thời của PyArg_ParseTuple() được sử dụng.

Khi chỉ sử dụng METH_VARARGS, hàm sẽ yêu cầu các tham số cấp Python được truyền vào dưới dạng một bộ dữ liệu được chấp nhận để phân tích cú pháp qua PyArg_ParseTuple(); thông tin thêm về chức năng này được cung cấp dưới đây.

Bit METH_KEYWORDS có thể được đặt trong trường thứ ba nếu đối số từ khóa được truyền cho hàm. Trong trường hợp này, hàm C phải chấp nhận tham số PyObject * thứ ba sẽ là từ điển các từ khóa. Sử dụng PyArg_ParseTupleAndKeywords() để phân tích các đối số cho hàm như vậy.

Bảng phương thức phải được tham chiếu trong cấu trúc định nghĩa mô-đun:

cấu trúc tĩnh PyModuleDef spam_module = {
    ...
    .m_methods = phương thức spam,
    ...
};

Ngược lại, cấu trúc này phải được chuyển tới trình thông dịch trong hàm khởi tạo của mô-đun. Hàm khởi tạo phải được đặt tên là PyInit_name(), trong đó name là tên của mô-đun và phải là mục duy nhất không phảistatic được xác định trong tệp mô-đun:

PyMODINIT_FUNC
PyInit_spam(void)
{
    trả về PyModuleDef_Init(&spam_module);
}

Lưu ý rằng PyMODINIT_FUNC khai báo hàm là kiểu trả về PyObject *, khai báo mọi khai báo liên kết đặc biệt mà nền tảng yêu cầu và đối với C++ khai báo hàm là extern "C".

PyInit_spam() được gọi khi mỗi trình thông dịch nhập mô-đun spam của nó lần đầu tiên. (Xem bên dưới để biết nhận xét về cách nhúng Python.) Một con trỏ tới định nghĩa mô-đun phải được trả về qua PyModuleDef_Init(), để máy nhập có thể tạo mô-đun và lưu trữ nó trong sys.modules.

Khi nhúng Python, hàm PyInit_spam() không được gọi tự động trừ khi có một mục trong bảng PyImport_Inittab. Để thêm mô-đun vào bảng khởi tạo, hãy sử dụng PyImport_AppendInittab(), tùy chọn sau đó là nhập mô-đun

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
chính(int argc, char *argv[])
{
    Trạng thái PyStatus;
    Cấu hình PyConfig;
    PyConfig_InitPythonConfig(&config);

    /* Thêm mô-đun tích hợp sẵn, trước Py_Initialize */
    if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
        fprintf(stderr, "Lỗi: không thể mở rộng bảng mô-đun dựng sẵn\n");
        thoát (1);
    }

    /* Truyền argv[0] tới trình thông dịch Python */
    status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
    if (PyStatus_Exception(trạng thái)) {
        ngoại lệ goto;
    }

    /* Khởi tạo trình thông dịch Python.  Yêu cầu.
       Nếu bước này không thành công thì đó sẽ là một lỗi nghiêm trọng. */
    trạng thái = Py_InitializeFromConfig(&config);
    if (PyStatus_Exception(trạng thái)) {
        ngoại lệ goto;
    }
    PyConfig_Clear(&config);

    /* Tùy chọn nhập mô-đun; cách khác,
       việc nhập có thể được hoãn lại cho đến khi tập lệnh được nhúng
       nhập khẩu nó. */
    PyObject *pmodule = PyImport_ImportModule("spam");
    nếu (! pmodule) {
        PyErr_Print();
        fprintf(stderr, "Lỗi: không thể nhập mô-đun 'spam'\n");
    }

    // ... sử dụng Python C API tại đây ...

    trả về 0;

  ngoại lệ:
     PyConfig_Clear(&config);
     Py_ExitStatusException(trạng thái);
}

Ghi chú

Nếu bạn khai báo một biến toàn cục hoặc một biến tĩnh cục bộ, mô-đun có thể gặp các tác dụng phụ ngoài ý muốn khi khởi tạo lại, chẳng hạn như khi xóa các mục nhập khỏi sys.modules hoặc nhập các mô-đun đã biên dịch vào nhiều trình thông dịch trong một quy trình (hoặc theo dõi fork() mà không có exec() can thiệp). Nếu trạng thái mô-đun chưa hoàn toàn là isolated, tác giả nên cân nhắc việc đánh dấu mô-đun là không hỗ trợ trình thông dịch phụ (thông qua Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED).

Một mô-đun ví dụ quan trọng hơn được bao gồm trong bản phân phối nguồn Python dưới dạng Modules/xxlimited.c. Tập tin này có thể được sử dụng làm mẫu hoặc chỉ đọc làm ví dụ.

1.5. Biên soạn và liên kết

Có hai việc nữa cần làm trước khi bạn có thể sử dụng tiện ích mở rộng mới của mình: biên dịch và liên kết nó với hệ thống Python. Nếu bạn sử dụng tải động, các chi tiết có thể phụ thuộc vào kiểu tải động mà hệ thống của bạn sử dụng; xem các chương về xây dựng mô-đun mở rộng (chương Xây dựng phần mở rộng C và C++) và thông tin bổ sung chỉ liên quan đến việc xây dựng trên Windows (chương Xây dựng tiện ích mở rộng C và C++ trên Windows) để biết thêm thông tin về điều này.

Nếu bạn không thể sử dụng tính năng tải động hoặc nếu bạn muốn biến mô-đun của mình thành một phần cố định của trình thông dịch Python, bạn sẽ phải thay đổi thiết lập cấu hình và xây dựng lại trình thông dịch. May mắn thay, điều này rất đơn giản trên Unix: chỉ cần đặt tệp của bạn (ví dụ spammodule.c) vào thư mục Modules/ của bản phân phối nguồn đã giải nén, thêm một dòng vào tệp Modules/Setup.local mô tả tệp của bạn:

thư rác spammodule.o

và xây dựng lại trình thông dịch bằng cách chạy make trong thư mục cấp cao nhất. Bạn cũng có thể chạy make trong thư mục con Modules/, nhưng trước tiên bạn phải xây dựng lại Makefile ở đó bằng cách chạy 'make Makefile'. (Điều này là cần thiết mỗi khi bạn thay đổi tệp Setup.)

Ví dụ: nếu mô-đun của bạn yêu cầu các thư viện bổ sung để liên kết, thì những thư viện này cũng có thể được liệt kê trên dòng trong tệp cấu hình:

thư rác spammodule.o -lX11

1.6. Gọi hàm Python từ C

Cho đến nay chúng ta đã tập trung vào việc tạo ra các hàm C có thể gọi được từ Python. Điều ngược lại cũng hữu ích: gọi các hàm Python từ C. Điều này đặc biệt xảy ra đối với các thư viện hỗ trợ cái gọi là hàm "gọi lại". Nếu giao diện C sử dụng lệnh gọi lại, thì Python tương đương thường cần cung cấp cơ chế gọi lại cho lập trình viên Python; việc triển khai sẽ yêu cầu gọi các hàm gọi lại Python từ lệnh gọi lại C. Các cách sử dụng khác cũng có thể tưởng tượng được.

May mắn thay, trình thông dịch Python có thể dễ dàng được gọi đệ quy và có một giao diện chuẩn để gọi hàm Python. (Tôi sẽ không tập trung vào cách gọi trình phân tích cú pháp Python bằng một chuỗi cụ thể làm đầu vào --- nếu bạn quan tâm, hãy xem cách triển khai tùy chọn dòng lệnh -c trong Modules/main.c từ mã nguồn Python.)

Gọi một hàm Python thật dễ dàng. Đầu tiên, bằng cách nào đó chương trình Python phải chuyển cho bạn đối tượng hàm Python. Bạn nên cung cấp một chức năng (hoặc một số giao diện khác) để thực hiện việc này. Khi hàm này được gọi, hãy lưu một con trỏ tới đối tượng hàm Python (hãy cẩn thận với Py_INCREF() nó!) trong một biến toàn cục --- hoặc bất cứ nơi nào bạn thấy phù hợp. Ví dụ: hàm sau có thể là một phần của định nghĩa mô-đun:

PyObject tĩnh *my_callback = NULL;

PyObject tĩnh *
my_set_callback(PyObject *dummy, PyObject *args)
{
    PyObject *kết quả = NULL;
    PyObject *temp;

    if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError, "tham số phải gọi được");
            trả về NULL;
        }
        Py_XINCREF(tạm thời);         /* Thêm tham chiếu đến lệnh gọi lại mới */
        Py_XDECREF(my_callback);  /* Loại bỏ lệnh gọi lại trước đó */
        my_callback = tạm thời;       /* Ghi nhớ lệnh gọi lại mới */
        /* Bản soạn sẵn để trả về "Không" */
        Py_INCREF(Py_None);
        kết quả = Py_None;
    }
    trả về kết quả;
}

Chức năng này phải được đăng ký với trình thông dịch bằng cờ METH_VARARGS; điều này được mô tả trong phần Bảng phương thức và hàm khởi tạo của mô-đun. Hàm PyArg_ParseTuple() và các đối số của nó được ghi lại trong phần Trích xuất tham số trong hàm mở rộng.

Các macro Py_XINCREF()Py_XDECREF() tăng/giảm số lượng tham chiếu của một đối tượng và an toàn khi có con trỏ NULL (nhưng lưu ý rằng temp sẽ không phải là NULL trong ngữ cảnh này). Thông tin thêm về chúng trong phần Số lượng tham chiếu.

Sau này khi đến lúc gọi hàm, bạn gọi hàm C là PyObject_CallObject(). Hàm này có hai đối số, cả hai đều trỏ tới các đối tượng Python tùy ý: hàm Python và danh sách đối số. Danh sách đối số phải luôn là một đối tượng tuple, có độ dài bằng số lượng đối số. Để gọi hàm Python không có đối số, hãy truyền vào NULL hoặc một bộ dữ liệu trống; để gọi nó bằng một đối số, hãy truyền một bộ dữ liệu đơn. Py_BuildValue() trả về một bộ dữ liệu khi chuỗi định dạng của nó bao gồm 0 hoặc nhiều mã định dạng giữa các dấu ngoặc đơn. Ví dụ:

int arg;
PyObject *arglist;
kết quả PyObject *;
...
đối số = 123;
...
/* Đã đến lúc gọi lại */
arglist = Py_BuildValue("(i)", arg);
kết quả = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);

PyObject_CallObject() trả về một con trỏ đối tượng Python: đây là giá trị trả về của hàm Python. PyObject_CallObject() là "trung lập về số lượng tham chiếu" đối với các đối số của nó. Trong ví dụ này, một bộ dữ liệu mới được tạo để dùng làm danh sách đối số, đó là Py_DECREF()-ed ngay sau lệnh gọi PyObject_CallObject().

Giá trị trả về của PyObject_CallObject() là "mới": đó là một đối tượng hoàn toàn mới hoặc là một đối tượng hiện có có số lượng tham chiếu đã được tăng lên. Vì vậy, trừ khi bạn muốn lưu nó trong một biến toàn cục, bằng cách nào đó bạn nên Py_DECREF() kết quả, thậm chí (đặc biệt!) nếu bạn không quan tâm đến giá trị của nó.

Tuy nhiên, trước khi thực hiện việc này, điều quan trọng là phải kiểm tra xem giá trị trả về có phải là NULL hay không. Nếu đúng như vậy, hàm Python sẽ kết thúc bằng cách đưa ra một ngoại lệ. Nếu mã C có tên PyObject_CallObject() được gọi từ Python thì giờ đây mã này sẽ trả về một dấu hiệu lỗi cho trình gọi Python của nó, để trình thông dịch có thể in dấu vết ngăn xếp hoặc mã Python đang gọi có thể xử lý ngoại lệ. Nếu điều này là không thể hoặc không mong muốn, thì nên xóa ngoại lệ bằng cách gọi PyErr_Clear(). Ví dụ:

nếu (kết quả == NULL)
    trả lại NULL; /* Truyền lại lỗi */
...sử dụng kết quả...
Py_DECREF(kết quả);

Tùy thuộc vào giao diện mong muốn của hàm gọi lại Python, bạn cũng có thể phải cung cấp danh sách đối số cho PyObject_CallObject(). Trong một số trường hợp, danh sách đối số cũng được chương trình Python cung cấp, thông qua cùng giao diện đã chỉ định hàm gọi lại. Sau đó nó có thể được lưu và sử dụng theo cách tương tự như đối tượng hàm. Trong các trường hợp khác, bạn có thể phải xây dựng một bộ dữ liệu mới để chuyển làm danh sách đối số. Cách đơn giản nhất để thực hiện việc này là gọi Py_BuildValue(). Ví dụ: nếu bạn muốn chuyển mã sự kiện tích hợp, bạn có thể sử dụng mã sau:

PyObject *arglist;
...
arglist = Py_BuildValue("(l)",  sự kiện);
kết quả = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
nếu (kết quả == NULL)
    trả lại NULL; /* Truyền lại lỗi */
/* Ở đây có thể sử dụng kết quả */
Py_DECREF(kết quả);

Lưu ý vị trí của Py_DECREF(arglist) ngay sau cuộc gọi, trước khi kiểm tra lỗi nhé! Cũng lưu ý rằng nói đúng ra thì mã này chưa hoàn chỉnh: Py_BuildValue() có thể hết bộ nhớ và điều này cần được kiểm tra.

Bạn cũng có thể gọi một hàm có đối số từ khóa bằng cách sử dụng PyObject_Call(), hàm này hỗ trợ các đối số và đối số từ khóa. Như trong ví dụ trên, chúng tôi sử dụng Py_BuildValue() để xây dựng từ điển.

PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
kết quả = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
nếu (kết quả == NULL)
    trả lại NULL; /* Truyền lại lỗi */
/* Ở đây có thể sử dụng kết quả */
Py_DECREF(kết quả);

1.7. Trích xuất tham số trong hàm mở rộng

Hàm PyArg_ParseTuple() được khai báo như sau

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

Đối số arg phải là một đối tượng tuple chứa danh sách đối số được truyền từ Python sang hàm C. Đối số format phải là một chuỗi định dạng, có cú pháp được giải thích bằng Phân tích đối số và xây dựng giá trị trong Hướng dẫn tham khảo API Python/C. Các đối số còn lại phải là địa chỉ của các biến có kiểu được xác định bởi chuỗi định dạng.

Lưu ý rằng mặc dù PyArg_ParseTuple() kiểm tra xem các đối số Python có loại được yêu cầu hay không, nhưng nó không thể kiểm tra tính hợp lệ của địa chỉ của các biến C được truyền cho lệnh gọi: nếu bạn mắc lỗi ở đó, mã của bạn có thể sẽ bị lỗi hoặc ít nhất là ghi đè các bit ngẫu nhiên trong bộ nhớ. Vì vậy hãy cẩn thận!

Lưu ý rằng mọi tham chiếu đối tượng Python được cung cấp cho người gọi đều là tham chiếu borrowed; không giảm số lượng tham chiếu của họ!

Một số cuộc gọi ví dụ

#define PY_SSIZE_T_CLEAN
#include <Python.h>
int ổn;
int tôi, j;
dài k, l;
const char *s;
kích thước Py_ssize_t;

được = PyArg_ParseTuple(args, ""); /* Không có đối số */
    /* Lệnh gọi Python: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* Một chuỗi */
    /* Lệnh gọi Python có thể có: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Hai đoạn dài và một chuỗi */
    /* Lệnh gọi Python có thể có: f(1, 2, 'ba') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* Một cặp số nguyên và một chuỗi, kích thước của nó cũng được trả về */
    /* Lệnh gọi Python có thể có: f((1, 2), 'ba') */
{
    const char *tập tin;
    const char *mode = "r";
    int bufsize = 0;
    ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
    /* Một chuỗi và tùy chọn một chuỗi khác và một số nguyên */
    /* Lệnh gọi Python có thể có:
       f('thư rác')
       f('thư rác', 'w')
       f('thư rác', 'wb', 100000) */
}
{
    int trái, trên, phải, dưới, h, v;
    ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
             &trái, &trên, &phải, &dưới, &h, &v);
    /*Một hình chữ nhật và một điểm */
    /* Lệnh gọi Python có thể có:
       f(((0, 0), (400, 300)), (10, 10)) */
}
{
    Py_complex c;
    ok = PyArg_ParseTuple(args, "D:myfunction", &c);
    /* một hàm phức tạp, đồng thời cung cấp tên hàm cho các lỗi */
    /* Lệnh gọi Python có thể có: myfunction(1+2j) */
}

1.8. Tham số từ khóa cho các hàm mở rộng

Hàm PyArg_ParseTupleAndKeywords() được khai báo như sau

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

Các tham số argformat giống hệt với các tham số của hàm PyArg_ParseTuple(). Tham số kwdict là từ điển các từ khóa được nhận làm tham số thứ ba từ thời gian chạy Python. Tham số kwlist là danh sách các chuỗi được kết thúc bằng NULL để xác định các tham số; tên được khớp với thông tin loại từ format từ trái sang phải. Khi thành công, PyArg_ParseTupleAndKeywords() trả về true, nếu không nó sẽ trả về false và đưa ra một ngoại lệ thích hợp.

Ghi chú

Không thể phân tích cú pháp các bộ dữ liệu lồng nhau khi sử dụng đối số từ khóa! Các tham số từ khóa được truyền vào mà không có trong kwlist sẽ khiến TypeError được nâng lên.

Đây là mô-đun ví dụ sử dụng từ khóa, dựa trên ví dụ của Geoff Philbrick (philbrick@hks.com):

#define PY_SSIZE_T_CLEAN
#include <Python.h>

PyObject tĩnh *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    điện áp int;
    const char *state = "cứng";
    const char *action = "voom";
    const char *type = "Xanh Na Uy";

    char tĩnh *kwlist[] = {"điện áp", "trạng thái", "hành động", "loại", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
                                     &điện áp, &trạng thái, &hành động, &loại))
        trả về NULL;

    printf("-- Con vẹt này sẽ không hoạt động %s nếu bạn đặt %i Vôn vào nó.\n",
           hành động, điện áp);
    printf("-- Bộ lông đáng yêu, %s -- Đó là %s!\n", type, state);

    Py_RETURN_NONE;
}

keywdarg_methods PyMethodDef tĩnh [] = {
    /* Việc truyền hàm là cần thiết vì các giá trị PyCFunction
     * chỉ lấy hai tham số PyObject* và keywdarg_parrot() nhận
     * ba.
     */
    {"con vẹt", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
     "In một tiểu phẩm đáng yêu ở đầu ra tiêu chuẩn."},
    {NULL, NULL, 0, NULL} /* trọng điểm */
};

cấu trúc tĩnh PyModuleDef keywdarg_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "keywdarg",
    .m_size = 0,
    .m_methods = keywdarg_methods,
};

PyMODINIT_FUNC
PyInit_keywdarg(void)
{
    trả về PyModuleDef_Init(&keywdarg_module);
}

1.9. Xây dựng giá trị tùy ý

Chức năng này tương tự như PyArg_ParseTuple(). Nó được khai báo như sau:

Định dạng PyObject *Py_BuildValue(const char *, ...);

Nó nhận dạng một tập hợp các đơn vị định dạng tương tự như các đơn vị được PyArg_ParseTuple() nhận dạng, nhưng các đối số (là đầu vào của hàm chứ không phải đầu ra) không được là con trỏ mà chỉ là giá trị. Nó trả về một đối tượng Python mới, phù hợp để trả về từ một hàm C được gọi từ Python.

Một điểm khác biệt với PyArg_ParseTuple(): trong khi cái sau yêu cầu đối số đầu tiên của nó phải là một bộ dữ liệu (vì danh sách đối số Python luôn được biểu diễn dưới dạng bộ dữ liệu bên trong), Py_BuildValue() không phải lúc nào cũng tạo một bộ dữ liệu. Nó chỉ xây dựng một bộ nếu chuỗi định dạng của nó chứa hai hoặc nhiều đơn vị định dạng. Nếu chuỗi định dạng trống, nó sẽ trả về None; nếu nó chứa chính xác một đơn vị định dạng, nó sẽ trả về bất kỳ đối tượng nào được mô tả bởi đơn vị định dạng đó. Để buộc nó trả về một bộ có kích thước 0 hoặc một, hãy đặt dấu ngoặc đơn vào chuỗi định dạng.

Ví dụ (ở bên trái cuộc gọi, ở bên phải giá trị Python kết quả):

Py_BuildValue("") Không có
Py_BuildValue("i", 123) 123
Py_BuildValue("iii", 123, 456, 789) (123, 456, 789)
Py_BuildValue("s", "xin chào") 'xin chào'
Py_BuildValue("y", "hello") b'hello'
Py_BuildValue("ss", "xin chào", "thế giới") ('xin chào', 'thế giới')
Py_BuildValue("s#", "xin chào", 4) 'chết tiệt'
Py_BuildValue("y#", "hello", 4) b'chết tiệt'
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))

1.10. Số lượng tham chiếu

Trong các ngôn ngữ như C hoặc C++, lập trình viên chịu trách nhiệm phân bổ động và phân bổ bộ nhớ trên heap. Trong C, việc này được thực hiện bằng cách sử dụng các hàm malloc()free(). Trong C++, các toán tử newdelete được sử dụng với ý nghĩa cơ bản giống nhau và chúng ta sẽ hạn chế thảo luận sau đây đối với trường hợp C.

Mỗi khối bộ nhớ được phân bổ bằng malloc() cuối cùng sẽ được trả về nhóm bộ nhớ khả dụng bằng chính xác một lệnh gọi tới free(). Điều quan trọng là phải gọi free() đúng lúc. Nếu địa chỉ của một khối bị quên nhưng free() không được gọi, bộ nhớ mà khối đó chiếm giữ không thể được sử dụng lại cho đến khi chương trình kết thúc. Đây được gọi là memory leak. Mặt khác, nếu một chương trình gọi free() cho một khối và sau đó tiếp tục sử dụng khối đó, nó sẽ tạo ra xung đột với việc sử dụng lại khối thông qua một lệnh gọi malloc() khác. Cái này được gọi là using freed memory. Nó có những hậu quả xấu tương tự như việc tham chiếu dữ liệu chưa được khởi tạo --- kết xuất lõi, kết quả sai, sự cố bí ẩn.

Nguyên nhân phổ biến gây rò rỉ bộ nhớ là các đường dẫn bất thường xuyên qua mã. Ví dụ: một hàm có thể phân bổ một khối bộ nhớ, thực hiện một số phép tính và sau đó giải phóng khối đó một lần nữa. Bây giờ, một thay đổi trong các yêu cầu đối với hàm có thể thêm một thử nghiệm vào phép tính để phát hiện tình trạng lỗi và có thể trả về sớm từ hàm. Bạn rất dễ quên giải phóng khối bộ nhớ được phân bổ khi thoát sớm này, đặc biệt là khi nó được thêm vào mã sau. Những rò rỉ như vậy, một khi đã xuất hiện, thường không bị phát hiện trong một thời gian dài: việc thoát lỗi chỉ được thực hiện trong một phần nhỏ của tất cả các cuộc gọi và hầu hết các máy hiện đại đều có nhiều bộ nhớ ảo, do đó, rò rỉ chỉ trở nên rõ ràng trong một quy trình chạy lâu dài sử dụng chức năng rò rỉ thường xuyên. Do đó, điều quan trọng là phải ngăn chặn rò rỉ xảy ra bằng cách có quy ước hoặc chiến lược mã hóa nhằm giảm thiểu loại lỗi này.

Vì Python sử dụng nhiều malloc()free() nên nó cần một chiến lược để tránh rò rỉ bộ nhớ cũng như sử dụng bộ nhớ được giải phóng. Phương pháp được chọn được gọi là reference counting. Nguyên tắc rất đơn giản: mọi đối tượng đều chứa một bộ đếm, bộ đếm này tăng lên khi một tham chiếu đến đối tượng được lưu trữ ở đâu đó và sẽ giảm đi khi một tham chiếu đến nó bị xóa. Khi bộ đếm đạt đến 0, tham chiếu cuối cùng đến đối tượng đã bị xóa và đối tượng được giải phóng.

Một chiến lược thay thế được gọi là automatic garbage collection. (Đôi khi, việc đếm tham chiếu còn được gọi là chiến lược thu gom rác, do đó tôi sử dụng "tự động" để phân biệt cả hai.) Ưu điểm lớn của việc thu gom rác tự động là người dùng không cần gọi free() một cách rõ ràng. (Một ưu điểm khác được khẳng định là sự cải thiện về tốc độ hoặc mức sử dụng bộ nhớ --- tuy nhiên đây không phải là sự thật khó hiểu.) Nhược điểm là đối với C, không có trình thu gom rác tự động di động thực sự, trong khi việc đếm tham chiếu có thể được triển khai một cách di động (miễn là có sẵn các hàm malloc()free() --- mà Tiêu chuẩn C đảm bảo). Có thể một ngày nào đó, một bộ thu gom rác tự động đủ di động sẽ có sẵn cho C. Cho đến lúc đó, chúng ta sẽ phải sống với số lượng tham chiếu.

Mặc dù Python sử dụng cách triển khai tính tham chiếu truyền thống nhưng nó cũng cung cấp một trình phát hiện chu trình hoạt động để phát hiện các chu trình tham chiếu. Điều này cho phép các ứng dụng không phải lo lắng về việc tạo các tham chiếu vòng tròn trực tiếp hoặc gián tiếp; đây là điểm yếu của việc thu thập rác được thực hiện chỉ bằng cách đếm tham chiếu. Chu trình tham chiếu bao gồm các đối tượng chứa các tham chiếu (có thể gián tiếp) đến chính chúng, sao cho mỗi đối tượng trong chu trình có số lượng tham chiếu khác 0. Việc triển khai tính tham chiếu thông thường không thể lấy lại bộ nhớ thuộc về bất kỳ đối tượng nào trong chu trình tham chiếu hoặc được tham chiếu từ các đối tượng trong chu trình, mặc dù không có thêm tham chiếu nào đến chính chu trình đó.

Trình phát hiện chu trình có thể phát hiện các chu trình rác và có thể lấy lại chúng. Mô-đun gc cung cấp cách chạy trình phát hiện (chức năng collect()), cũng như các giao diện cấu hình và khả năng vô hiệu hóa trình phát hiện khi chạy.

1.10.1. Đếm tham chiếu trong Python

Có hai macro, Py_INCREF(x)Py_DECREF(x), xử lý việc tăng và giảm số lượng tham chiếu. Py_DECREF() cũng giải phóng đối tượng khi số đếm về 0. Để linh hoạt, nó không gọi trực tiếp free() --- thay vào đó, nó thực hiện cuộc gọi thông qua một con trỏ hàm trong type object của đối tượng. Với mục đích này (và những mục đích khác), mọi đối tượng cũng chứa một con trỏ tới đối tượng kiểu của nó.

Câu hỏi lớn bây giờ vẫn là: khi nào nên sử dụng Py_INCREF(x)Py_DECREF(x)? Trước tiên hãy giới thiệu một số thuật ngữ. Không ai "sở hữu" một đồ vật; tuy nhiên, bạn có thể own a reference tới một đối tượng. Số lượng tham chiếu của một đối tượng hiện được xác định là số lượng tham chiếu được sở hữu đối với nó. Chủ sở hữu của một tài liệu tham khảo có trách nhiệm gọi Py_DECREF() khi tài liệu tham khảo không còn cần thiết nữa. Quyền sở hữu của một tài liệu tham khảo có thể được chuyển giao. Có ba cách để loại bỏ một tham chiếu đã sở hữu: chuyển nó đi, lưu trữ nó hoặc gọi Py_DECREF(). Việc quên loại bỏ tham chiếu đã sở hữu sẽ tạo ra rò rỉ bộ nhớ.

Cũng có thể borrow [2] một tham chiếu đến một đối tượng. Người mượn tài liệu tham khảo không nên gọi Py_DECREF(). Người mượn không được giữ đồ vật lâu hơn chủ sở hữu đồ vật đó. Việc sử dụng tham chiếu mượn sau khi chủ sở hữu đã loại bỏ nó có nguy cơ sử dụng bộ nhớ đã giải phóng và nên tránh hoàn toàn [3].

Ưu điểm của việc mượn so với việc sở hữu một tham chiếu là bạn không cần phải xử lý tham chiếu trên tất cả các đường dẫn có thể thông qua mã --- nói cách khác, với một tham chiếu được mượn, bạn không gặp rủi ro bị rò rỉ khi thực hiện thoát sớm. Nhược điểm của việc vay mượn thay vì sở hữu là có một số tình huống tế nhị trong đó mã có vẻ chính xác có thể được sử dụng tham chiếu mượn sau khi chủ sở hữu mà nó được mượn trên thực tế đã vứt bỏ nó.

Một tham chiếu mượn có thể được thay đổi thành một tham chiếu sở hữu bằng cách gọi Py_INCREF(). Điều này không ảnh hưởng đến trạng thái của chủ sở hữu mà tham chiếu được mượn --- nó tạo ra một tham chiếu được sở hữu mới và trao toàn bộ trách nhiệm của chủ sở hữu (chủ sở hữu mới phải loại bỏ tham chiếu một cách hợp lý cũng như chủ sở hữu trước đó).

1.10.2. Quy tắc sở hữu

Bất cứ khi nào một tham chiếu đối tượng được truyền vào hoặc ra khỏi một hàm, thì đó là một phần đặc tả giao diện của hàm cho dù quyền sở hữu có được chuyển giao cùng với tham chiếu đó hay không.

Hầu hết các hàm trả về một tham chiếu đến một đối tượng sẽ chuyển quyền sở hữu với tham chiếu đó. Đặc biệt, tất cả các hàm có chức năng tạo đối tượng mới, chẳng hạn như PyLong_FromLong()Py_BuildValue(), đều chuyển quyền sở hữu cho người nhận. Ngay cả khi đối tượng không thực sự mới, bạn vẫn nhận được quyền sở hữu một tham chiếu mới tới đối tượng đó. Ví dụ: PyLong_FromLong() duy trì bộ nhớ đệm gồm các giá trị phổ biến và có thể trả về tham chiếu đến một mục được lưu trong bộ nhớ đệm.

Nhiều hàm trích xuất đối tượng từ các đối tượng khác cũng chuyển quyền sở hữu bằng tham chiếu, chẳng hạn như PyObject_GetAttrString(). Tuy nhiên, ở đây, hình ảnh chưa rõ ràng vì một số thủ tục phổ biến là ngoại lệ: PyTuple_GetItem(), PyList_GetItem(), PyDict_GetItem()PyDict_GetItemString() đều trả về các tham chiếu mà bạn mượn từ bộ dữ liệu, danh sách hoặc từ điển.

Hàm PyImport_AddModule() cũng trả về một tham chiếu mượn, mặc dù nó thực sự có thể tạo ra đối tượng mà nó trả về: điều này có thể thực hiện được vì một tham chiếu thuộc sở hữu của đối tượng được lưu trữ trong sys.modules.

Khi bạn chuyển một tham chiếu đối tượng sang một hàm khác, nói chung, hàm đó sẽ mượn tham chiếu từ bạn --- nếu cần lưu trữ, nó sẽ sử dụng Py_INCREF() để trở thành chủ sở hữu độc lập. Có chính xác hai trường hợp ngoại lệ quan trọng đối với quy tắc này: PyTuple_SetItem()PyList_SetItem(). Các chức năng này tiếp quản quyền sở hữu đối tượng được chuyển cho chúng --- ngay cả khi chúng thất bại! (Lưu ý rằng PyDict_SetItem() và bạn bè không chiếm quyền sở hữu --- họ "bình thường.")

Khi một hàm C được gọi từ Python, nó sẽ mượn các tham chiếu đến các đối số của nó từ người gọi. Người gọi sở hữu một tham chiếu đến đối tượng, do đó thời gian tồn tại của tham chiếu mượn được đảm bảo cho đến khi hàm trả về. Chỉ khi một tham chiếu mượn như vậy phải được lưu trữ hoặc chuyển đi, nó phải được chuyển thành tham chiếu sở hữu bằng cách gọi Py_INCREF().

Tham chiếu đối tượng được trả về từ hàm C được gọi từ Python phải là tham chiếu thuộc sở hữu --- quyền sở hữu được chuyển từ hàm sang người gọi nó.

1.10.3. Băng mỏng

Có một số tình huống mà việc sử dụng tài liệu tham khảo mượn dường như vô hại lại có thể dẫn đến vấn đề. Tất cả những điều này đều liên quan đến lời gọi ngầm của trình thông dịch, điều này có thể khiến chủ sở hữu của một tham chiếu loại bỏ nó.

Trường hợp đầu tiên và quan trọng nhất cần biết là sử dụng Py_DECREF() trên một đối tượng không liên quan trong khi mượn tham chiếu đến một mục danh sách. Ví dụ:

trống rỗng
lỗi (danh sách PyObject *)
{
    PyObject *item = PyList_GetItem(list, 0);

    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(mục, thiết bị xuất chuẩn, 0); /* BUG! */
}

Trước tiên, hàm này mượn tham chiếu đến list[0], sau đó thay thế list[1] bằng giá trị 0 và cuối cùng in tham chiếu đã mượn. Có vẻ vô hại phải không? Nhưng không phải vậy!

Hãy theo dõi luồng điều khiển vào PyList_SetItem(). Danh sách sở hữu các tham chiếu đến tất cả các mục của nó, vì vậy khi mục 1 được thay thế, nó phải loại bỏ mục 1 ban đầu. Bây giờ, hãy giả sử mục 1 ban đầu là một thể hiện của một lớp do người dùng định nghĩa và hãy giả sử thêm rằng lớp đó đã xác định một phương thức __del__(). Nếu phiên bản lớp này có số tham chiếu là 1, việc xử lý nó sẽ gọi phương thức __del__() của nó. Trong nội bộ, PyList_SetItem() gọi Py_DECREF() trên mục được thay thế, gọi hàm tp_dealloc tương ứng của mục được thay thế. Trong quá trình giải phóng, tp_dealloc gọi tp_finalize, được ánh xạ tới phương thức __del__() cho các phiên bản lớp (xem PEP 442). Toàn bộ chuỗi này diễn ra đồng bộ trong lệnh gọi PyList_SetItem().

Vì được viết bằng Python nên phương thức __del__() có thể thực thi mã Python tùy ý. Có lẽ nó có thể làm gì đó để vô hiệu hóa tham chiếu đến item trong bug()? Bạn đặt cược! Giả sử rằng danh sách được chuyển vào bug() có thể truy cập được bằng phương thức __del__(), nó có thể thực thi một câu lệnh có tác dụng của del list[0] và giả sử đây là tham chiếu cuối cùng đến đối tượng đó, nó sẽ giải phóng bộ nhớ liên kết với nó, do đó làm mất hiệu lực item.

Giải pháp, khi bạn biết nguồn gốc của vấn đề, thật dễ dàng: tạm thời tăng số lượng tham chiếu. Phiên bản chính xác của hàm có nội dung:

trống rỗng
no_bug(danh sách PyObject *)
{
    PyObject *item = PyList_GetItem(list, 0);

    Py_INCREF(vật phẩm);
    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(mục, thiết bị xuất chuẩn, 0);
    Py_DECREF(vật phẩm);
}

Đây là một câu chuyện có thật. Một phiên bản cũ hơn của Python chứa các biến thể của lỗi này và ai đó đã dành một lượng thời gian đáng kể vào trình gỡ lỗi C để tìm ra lý do tại sao các phương thức __del__() của anh ta lại thất bại...

Trường hợp thứ hai có vấn đề với tham chiếu mượn là một biến thể liên quan đến các luồng. Thông thường, nhiều luồng trong trình thông dịch Python không thể cản trở nhau vì có global lock bảo vệ toàn bộ không gian đối tượng của Python. Tuy nhiên, có thể tạm thời giải phóng khóa này bằng macro Py_BEGIN_ALLOW_THREADS và lấy lại nó bằng Py_END_ALLOW_THREADS. Điều này thường xảy ra xung quanh việc chặn các cuộc gọi I/O, để cho phép các luồng khác sử dụng bộ xử lý trong khi chờ I/O hoàn tất. Rõ ràng, hàm sau có cùng vấn đề với hàm trước:

trống rỗng
lỗi (danh sách PyObject *)
{
    PyObject *item = PyList_GetItem(list, 0);
    Py_BEGIN_ALLOW_THREADS
    ...một số cuộc gọi I/O đang chặn...
    Py_END_ALLOW_THREADS
    PyObject_Print(mục, thiết bị xuất chuẩn, 0); /* BUG! */
}

1.10.4. Con trỏ NULL

Nói chung, các hàm lấy tham chiếu đối tượng làm đối số không yêu cầu bạn chuyển các con trỏ NULL cho chúng và sẽ kết xuất lõi (hoặc gây ra kết xuất lõi sau này) nếu bạn làm như vậy. Các hàm trả về tham chiếu đối tượng thường chỉ trả về NULL để cho biết đã xảy ra ngoại lệ. Lý do không kiểm tra đối số NULL là vì các hàm thường chuyển đối tượng mà chúng nhận được sang hàm khác --- nếu mỗi hàm kiểm tra NULL thì sẽ có rất nhiều kiểm tra dư thừa và mã sẽ chạy chậm hơn.

Tốt hơn là chỉ nên kiểm tra NULL ở "nguồn:" khi nhận được một con trỏ có thể là NULL, chẳng hạn như từ malloc() hoặc từ một hàm có thể đưa ra một ngoại lệ.

Các macro Py_INCREF()Py_DECREF() không kiểm tra con trỏ NULL --- tuy nhiên, các biến thể Py_XINCREF()Py_XDECREF() của chúng lại làm như vậy.

Các macro để kiểm tra một loại đối tượng cụ thể (Pytype_Check()) không kiểm tra các con trỏ NULL --- một lần nữa, có nhiều mã gọi một số trong số này liên tiếp để kiểm tra một đối tượng dựa trên nhiều loại dự kiến ​​khác nhau và điều này sẽ tạo ra các thử nghiệm dư thừa. Không có biến thể nào được kiểm tra NULL.

Cơ chế gọi hàm C đảm bảo rằng danh sách đối số được truyền cho hàm C (args trong các ví dụ) không bao giờ là NULL --- trên thực tế, nó đảm bảo rằng nó luôn là một bộ [4].

Sẽ là một lỗi nghiêm trọng khi để con trỏ NULL "thoát" tới người dùng Python.

1.11. Viết phần mở rộng bằng C++

Có thể viết các module mở rộng bằng C++. Một số hạn chế được áp dụng. Nếu chương trình chính (trình thông dịch Python) được biên dịch và liên kết bởi trình biên dịch C, thì không thể sử dụng các đối tượng toàn cục hoặc tĩnh có hàm tạo. Đây không phải là vấn đề nếu chương trình chính được liên kết bởi trình biên dịch C++. Các hàm sẽ được trình thông dịch Python gọi (đặc biệt là các hàm khởi tạo mô-đun) phải được khai báo bằng extern "C". Không cần thiết phải đặt các tệp tiêu đề Python trong extern "C" {...} --- chúng đã sử dụng biểu mẫu này nếu ký hiệu __cplusplus được xác định (tất cả các trình biên dịch C++ gần đây đều xác định ký hiệu này).

1.12. Cung cấp C API cho Mô-đun mở rộng

Nhiều mô-đun mở rộng chỉ cung cấp các hàm và kiểu mới để sử dụng từ Python, nhưng đôi khi mã trong mô-đun mở rộng có thể hữu ích cho các mô-đun mở rộng khác. Ví dụ: một mô-đun mở rộng có thể triển khai loại "bộ sưu tập" hoạt động giống như danh sách không có thứ tự. Giống như loại danh sách Python tiêu chuẩn có C API cho phép các mô-đun mở rộng tạo và thao tác danh sách, loại bộ sưu tập mới này phải có một tập hợp các hàm C để thao tác trực tiếp từ các mô-đun mở rộng khác.

Thoạt nhìn, điều này có vẻ dễ dàng: chỉ cần viết các hàm (tất nhiên là không khai báo chúng static), cung cấp một tệp tiêu đề thích hợp và ghi lại C API. Và trên thực tế, điều này sẽ hoạt động nếu tất cả các mô-đun mở rộng luôn được liên kết tĩnh với trình thông dịch Python. Tuy nhiên, khi các mô-đun được sử dụng làm thư viện dùng chung, các ký hiệu được xác định trong một mô-đun có thể không hiển thị đối với mô-đun khác. Chi tiết về khả năng hiển thị phụ thuộc vào hệ điều hành; một số hệ thống sử dụng một không gian tên chung cho trình thông dịch Python và tất cả các mô-đun mở rộng (ví dụ: Windows), trong khi các hệ thống khác yêu cầu danh sách rõ ràng các ký hiệu được nhập tại thời điểm liên kết mô-đun (AIX là một ví dụ) hoặc đưa ra lựa chọn các chiến lược khác nhau (hầu hết các Unices). Và ngay cả khi các ký hiệu hiển thị trên toàn cầu, mô-đun có chức năng mà người ta muốn gọi có thể chưa được tải!

Do đó, tính di động không yêu cầu đưa ra bất kỳ giả định nào về khả năng hiển thị mã vạch. Điều này có nghĩa là tất cả các ký hiệu trong mô-đun mở rộng phải được khai báo là static, ngoại trừ chức năng khởi tạo của mô-đun, để tránh xung đột tên với các mô-đun mở rộng khác (như đã thảo luận trong phần Bảng phương thức và hàm khởi tạo của mô-đun). Và điều đó có nghĩa là các ký hiệu mà should có thể truy cập được từ các mô-đun mở rộng khác phải được xuất theo một cách khác.

Python cung cấp một cơ chế đặc biệt để truyền thông tin cấp độ C (con trỏ) từ mô-đun mở rộng này sang mô-đun mở rộng khác: Capsules. Capsule là kiểu dữ liệu Python lưu trữ một con trỏ (void*). Các viên nang chỉ có thể được tạo và truy cập thông qua C API của chúng, nhưng chúng có thể được truyền đi khắp nơi như bất kỳ đối tượng Python nào khác. Đặc biệt, chúng có thể được gán cho một tên trong không gian tên của mô-đun mở rộng. Sau đó, các mô-đun mở rộng khác có thể nhập mô-đun này, truy xuất giá trị của tên này và sau đó truy xuất con trỏ từ Capsule.

Có nhiều cách để sử dụng Capsule để xuất C API của mô-đun mở rộng. Mỗi hàm có thể có Capsule riêng hoặc tất cả các con trỏ C API có thể được lưu trữ trong một mảng có địa chỉ được xuất bản trong Capsule. Và các nhiệm vụ lưu trữ và truy xuất con trỏ khác nhau có thể được phân phối theo nhiều cách khác nhau giữa mô-đun cung cấp mã và mô-đun máy khách.

Cho dù bạn chọn phương pháp nào, điều quan trọng là phải đặt tên cho Viên nang của bạn đúng cách. Hàm PyCapsule_New() lấy tham số tên (const char*); bạn được phép chuyển tên NULL nhưng chúng tôi đặc biệt khuyến khích bạn chỉ định tên. Các Capsule được đặt tên chính xác sẽ cung cấp mức độ an toàn về kiểu thời gian chạy; không có cách nào khả thi để phân biệt một Capsule không có tên với một Capsule khác.

Đặc biệt, các Capsule được sử dụng để hiển thị API C phải được đặt tên theo quy ước sau:

modulename.attributename

Chức năng tiện lợi PyCapsule_Import() giúp bạn dễ dàng tải C API được cung cấp qua Capsule, nhưng chỉ khi tên của Capsule phù hợp với quy ước này. Hành vi này mang lại cho người dùng C API mức độ chắc chắn cao rằng Capsule họ tải có chứa C API chính xác.

Ví dụ sau đây minh họa một cách tiếp cận đặt phần lớn gánh nặng lên người viết mô-đun xuất, phù hợp với các mô-đun thư viện thường được sử dụng. Nó lưu trữ tất cả các con trỏ C API (chỉ một trong ví dụ!) trong một mảng các con trỏ void trở thành giá trị của Capsule. Tệp tiêu đề tương ứng với mô-đun cung cấp một macro đảm nhiệm việc nhập mô-đun và truy xuất các con trỏ C API của nó; mô-đun máy khách chỉ phải gọi macro này trước khi truy cập C API.

Mô-đun xuất là bản sửa đổi của mô-đun spam từ phần Một ví dụ đơn giản. Hàm spam.system() không gọi trực tiếp hàm system() trong thư viện C mà là hàm PySpam_System(), tất nhiên hàm này sẽ thực hiện điều gì đó phức tạp hơn trong thực tế (chẳng hạn như thêm "spam" vào mỗi lệnh). Chức năng PySpam_System() này cũng được xuất sang các mô-đun mở rộng khác.

Hàm PySpam_System() là một hàm C đơn giản, được khai báo static giống như mọi hàm khác:

int tĩnh
PySpam_System(const char *lệnh)
{
    hệ thống trả về (lệnh);
}

Hàm spam_system() được sửa đổi một cách tầm thường

PyObject tĩnh *
spam_system(PyObject *self, PyObject *args)
{
    lệnh const char *;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        trả lại NULL;
    sts = PySpam_System(lệnh);
    trả về PyLong_FromLong(sts);
}

Ở phần đầu của mô-đun, ngay sau dòng

#include <Python.h>

phải thêm hai dòng nữa

#define SPAM_MODULE
#include "spammodule.h"

#define được sử dụng để thông báo cho tệp tiêu đề rằng nó đang được đưa vào mô-đun xuất chứ không phải mô-đun máy khách. Cuối cùng, hàm mod_exec của mô-đun phải đảm nhiệm việc khởi tạo mảng con trỏ C API:

int tĩnh
spam_module_exec(PyObject *m)
{
    khoảng trống tĩnh *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    /* Khởi tạo mảng con trỏ C API */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* Tạo một Capsule chứa địa chỉ của mảng con trỏ API */
    c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);

    if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
        trả về -1;
    }

    trả về 0;
}

Lưu ý rằng PySpam_API được khai báo là static; nếu không mảng con trỏ sẽ biến mất khi PyInit_spam() kết thúc!

Phần lớn công việc nằm trong tệp tiêu đề spammodule.h, trông giống như sau:

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
bên ngoài "C" {
#endif

/* Tệp tiêu đề cho mô-đun thư rác */

/* Hàm API của C */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (lệnh const char *)

/* Tổng số con trỏ C API */
#define PySpam_API_pointers 1


#ifdef SPAM_MODULE
/* Phần này được sử dụng khi biên dịch spammodule.c */

PySpam_System_RETURN PySpam_System tĩnh PySpam_System_PROTO;

#else
/* Phần này được sử dụng trong các module sử dụng API của spammodule */

khoảng trống tĩnh **PySpam_API;

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

/* Trả về -1 nếu có lỗi, 0 nếu thành công.
 * PyCapsule_Import sẽ đặt ngoại lệ nếu có lỗi.
 */
int tĩnh
import_spam(void)
{
    PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
    trả lại (PySpam_API != NULL)? 0 : -1;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !d xác định(Py_SPAMMODULE_H) */

Tất cả những gì mô-đun máy khách phải làm để có quyền truy cập vào hàm PySpam_System() là gọi hàm (hay đúng hơn là macro) import_spam() trong hàm mod_exec của nó:

int tĩnh
client_module_exec(PyObject *m)
{
    if (import_spam() < 0) {
        trả về -1;
    }
    /* Việc khởi tạo bổ sung có thể xảy ra ở đây */
    trả về 0;
}

Nhược điểm chính của phương pháp này là tệp spammodule.h khá phức tạp. Tuy nhiên, cấu trúc cơ bản của mỗi hàm được xuất là giống nhau nên chỉ phải học một lần.

Cuối cùng, cần đề cập rằng Capsule cung cấp chức năng bổ sung, đặc biệt hữu ích cho việc cấp phát bộ nhớ và phân bổ lại con trỏ được lưu trữ trong Capsule. Chi tiết được mô tả trong Hướng dẫn tham khảo Python/C API trong phần Viên nang và trong cách triển khai Capsules (tệp Include/pycapsule.hObjects/pycapsule.c trong bản phân phối mã nguồn Python).

Chú thích cuối trang