Буферный протокол

Некоторые объекты, доступные в Python, обеспечивают доступ к базовому массиву памяти или буферу. К таким объектам относятся встроенные bytes и bytearray, а также некоторые расширенные типы, например array.array. Сторонние библиотеки могут определять собственные типы для специальных целей, таких как обработка изображений или численный анализ.

Хотя каждый из этих типов имеет свою семантику, их объединяет то, что они опираются на возможно большой буфер памяти. В некоторых ситуациях желательно обращаться к этому буферу напрямую, без промежуточного копирования.

Python предоставляет такую возможность на уровне C в виде протокола buffer protocol. У этого протокола есть две стороны:

  • со стороны производителя тип может экспортировать «интерфейс буфера», который позволяет объектам этого типа раскрывать информацию о лежащем в их основе буфере. Этот интерфейс описан в разделе Структуры буферных объектов;

  • На стороне потребителя доступно несколько средств для получения указателя на исходные данные объекта (например, параметр метода).

Простые объекты, такие как bytes и bytearray, отображают свой базовый буфер в байт-ориентированной форме. Возможны и другие формы; например, элементы, выставляемые array.array, могут быть многобайтовыми значениями.

Примером использования буферного интерфейса является метод write() для файловых объектов: любой объект, который может экспортировать серию байтов через буферный интерфейс, может быть записан в файл. В то время как метод write() нуждается только в доступе на чтение к внутреннему содержимому переданного ему объекта, другие методы, такие как readinto(), нуждаются в доступе на запись к содержимому своего аргумента. Интерфейс буфера позволяет объектам выборочно разрешать или запрещать экспорт буферов, предназначенных только для чтения и записи.

У потребителя интерфейса буфера есть два способа получить буфер над целевым объектом:

В обоих случаях вызов PyBuffer_Release() должен происходить, когда буфер больше не нужен. В противном случае это может привести к различным проблемам, таким как утечка ресурсов.

Структура буфера

Буферные структуры (или просто «буферы») полезны как способ передачи двоичных данных из другого объекта программисту Python. Их также можно использовать в качестве механизма нарезки с нулевым копированием. Используя их способность ссылаться на блок памяти, можно легко предоставить программисту Python любые данные. Это может быть большой постоянный массив в расширении языка C, необработанный блок памяти для манипуляций перед передачей в библиотеку операционной системы или структурированные данные в их родном формате in-memory.

В отличие от большинства типов данных, открываемых интерпретатором Python, буферы - это не указатели PyObject, а простые структуры на языке C. Это позволяет создавать и копировать их очень просто. Когда требуется общая обертка вокруг буфера, можно создать объект memoryview.

Краткие инструкции по написанию экспортируемого объекта см. в разделе Buffer Object Structures. О получении буфера см. в разделе PyObject_GetBuffer().

type Py_buffer
Часть Стабильный ABI (включая всех членов) с версии 3.11.
void *buf

Указатель на начало логической структуры, описываемой полями буфера. Это может быть любое место в базовом блоке физической памяти экспортера. Например, при отрицательном значении strides значение может указывать на конец блока памяти.

Для массивов contiguous значение указывает на начало блока памяти.

PyObject *obj

Новая ссылка на экспортируемый объект. Ссылка принадлежит потребителю и автоматически освобождается (т. е. счетчик ссылок декрементируется) и устанавливается в NULL по PyBuffer_Release(). Это поле эквивалентно возвращаемому значению любой стандартной функции C-API.

В качестве особого случая, для временных буферов, которые обернуты PyMemoryView_FromBuffer() или PyBuffer_FillInfo(), это поле равно NULL. В общем случае экспортируемые объекты НЕ ДОЛЖНЫ использовать эту схему.

Py_ssize_t len

product(shape) * itemsize. Для смежных массивов это длина базового блока памяти. Для несмежных массивов это длина, которую имела бы логическая структура, если бы она была скопирована в смежное представление.

Доступ к ((char *)buf)[0] up to ((char *)buf)[len-1] возможен только в том случае, если буфер был получен запросом, гарантирующим его непрерывность. В большинстве случаев таким запросом будет PyBUF_SIMPLE или PyBUF_WRITABLE.

int readonly

Индикатор того, является ли буфер доступным только для чтения. Это поле контролируется флагом PyBUF_WRITABLE.

Py_ssize_t itemsize

Размер элемента в байтах для одного элемента. То же, что и значение struct.calcsize(), вызываемое для не``NULL`` format значения.

Важное исключение: Если потребитель запрашивает буфер без флага PyBUF_FORMAT, format будет установлен в NULL, но itemsize по-прежнему будет иметь значение для исходного формата.

Если shape присутствует, то равенство product(shape) * itemsize == len по-прежнему выполняется, и потребитель может использовать itemsize для навигации по буферу.

Если shape стал NULL в результате запроса PyBUF_SIMPLE или PyBUF_WRITABLE, потребитель должен игнорировать itemsize и принять itemsize == 1.

char *format

Строка с окончанием NULL в синтаксисе стиля модуля struct, описывающая содержимое одного элемента. Если это NULL, то предполагается "B" (беззнаковые байты).

Это поле управляется флагом PyBUF_FORMAT.

int ndim

Количество измерений, которые память представляет в виде n-мерного массива. Если это значение равно 0, то buf указывает на один элемент, представляющий скаляр. В этом случае shape, strides и suboffsets ДОЛЖНЫ быть NULL. Максимальное количество измерений задается значением PyBUF_MAX_NDIM.

Py_ssize_t *shape

Массив Py_ssize_t длины ndim, указывающий на форму памяти в виде n-мерного массива. Обратите внимание, что shape[0] * ... * shape[ndim-1] * itemsize ДОЛЖЕН быть равен len.

Значения формы ограничены shape[n] >= 0. Случай shape[n] == 0 требует особого внимания. Смотрите complex arrays для получения дополнительной информации.

Массив форм доступен потребителю только для чтения.

Py_ssize_t *strides

Массив Py_ssize_t длины ndim, задающий количество байтов, которые нужно пропустить, чтобы перейти к новому элементу в каждом измерении.

Значения страйдов могут быть любыми целыми числами. Для обычных массивов значения страйдов обычно положительные, но потребитель ДОЛЖЕН уметь обрабатывать случай strides[n] <= 0. Для получения дополнительной информации см. раздел complex arrays.

Массив strides доступен потребителю только для чтения.

Py_ssize_t *suboffsets

Массив Py_ssize_t длины ndim. Если suboffsets[n] >= 0, то значения, хранящиеся в n-ом измерении, являются указателями, а значение suboffset определяет, сколько байт нужно добавить к каждому указателю после отмены ссылки. Отрицательное значение suboffset указывает на то, что отмены ссылки не должно быть (перемещение в непрерывном блоке памяти).

Если все подмножества отрицательны (т.е. отсылка не требуется), то это поле должно быть NULL (значение по умолчанию).

Этот тип представления массива используется библиотекой Python Imaging Library (PIL). Дополнительную информацию о том, как получить доступ к элементам такого массива, см. в разделе complex arrays.

Массив suboffsets доступен только для чтения потребителю.

void *internal

Это значение предназначено для внутреннего использования экспортирующим объектом. Например, он может быть пересчитан экспортером как целое число и использоваться для хранения флагов о том, должны ли массивы shape, strides и suboffsets быть освобождены при освобождении буфера. Потребитель НЕ ДОЛЖЕН изменять это значение.

Константы:

PyBUF_MAX_NDIM

Максимальное количество измерений, которое представляет память. Экспортеры ДОЛЖНЫ соблюдать это ограничение, потребители многомерных буферов ДОЛЖНЫ иметь возможность обрабатывать до PyBUF_MAX_NDIM размеров. В настоящее время установлено значение 64.

Типы буферных запросов

Буферы обычно получают, отправляя запрос на буфер объекту-экспортеру через PyObject_GetBuffer(). Поскольку сложность логической структуры памяти может сильно различаться, потребитель использует аргумент flags для указания точного типа буфера, с которым он может работать.

Все поля Py_buffer однозначно определяются типом запроса.

поля, не зависящие от запроса

Следующие поля не зависят от флагов и всегда должны быть заполнены правильными значениями: obj, buf, len, itemsize, ndim.

доступно для чтения, формат

PyBUF_WRITABLE

Управляет полем readonly. Если установлено, экспортер ДОЛЖЕН предоставить буфер, доступный для записи, или сообщить о неудаче. В противном случае экспортер МОЖЕТ предоставить либо буфер, доступный только для чтения, либо для записи, но этот выбор ДОЛЖЕН быть согласован для всех потребителей.

PyBUF_FORMAT

Управляет полем format. Если установлено, это поле ДОЛЖНО быть заполнено правильно. В противном случае это поле ДОЛЖНО быть NULL.

PyBUF_WRITABLE может быть |присоединен к любому из флагов в следующем разделе. Поскольку PyBUF_SIMPLE определен как 0, PyBUF_WRITABLE можно использовать как отдельный флаг для запроса простого буфера с возможностью записи.

PyBUF_FORMAT может быть |присоединен к любому из флагов, кроме PyBUF_SIMPLE. Последний уже подразумевает формат B (беззнаковые байты).

форма, полосы, подмножества

Флаги, управляющие логической структурой памяти, перечислены в порядке убывания сложности. Обратите внимание, что каждый флаг содержит все биты флагов, расположенных ниже него.

Запрос

форма

страйды

поднаборы

PyBUF_INDIRECT

да

да

при необходимости

PyBUF_STRIDES

да

да

NULL

PyBUF_ND

да

NULL

NULL

PyBUF_SIMPLE

NULL

NULL

NULL

запросы на смежность

C или Fortran contiguity может быть запрошен явно, с информацией о страйде и без нее. Без информации о строках буфер должен быть C-континуальным.

Запрос

форма

страйды

поднаборы

contig

PyBUF_C_CONTIGUOUS

да

да

NULL

C

PyBUF_F_CONTIGUOUS

да

да

NULL

F

PyBUF_ANY_CONTIGUOUS

да

да

NULL

C или F

PyBUF_ND

да

NULL

NULL

C

составные запросы

Все возможные запросы полностью определяются некоторой комбинацией флагов, приведенных в предыдущем разделе. Для удобства в буферном протоколе часто используемые комбинации представлены в виде отдельных флагов.

В следующей таблице U означает неопределенную смежность. Потребитель должен вызвать PyBuffer_IsContiguous(), чтобы определить смежность.

Запрос

форма

страйды

поднаборы

contig

readonly

формат

PyBUF_FULL

да

да

при необходимости

U

0

да

PyBUF_FULL_RO

да

да

при необходимости

U

1 или 0

да

PyBUF_RECORDS

да

да

NULL

U

0

да

PyBUF_RECORDS_RO

да

да

NULL

U

1 или 0

да

PyBUF_STRIDED

да

да

NULL

U

0

NULL

PyBUF_STRIDED_RO

да

да

NULL

U

1 или 0

NULL

PyBUF_CONTIG

да

NULL

NULL

C

0

NULL

PyBUF_CONTIG_RO

да

NULL

NULL

C

1 или 0

NULL

Сложные массивы

NumPy-стиль: форма и полосы

Логическая структура массивов в стиле NumPy определяется itemsize, ndim, shape и strides.

Если ndim == 0, то ячейка памяти, на которую указывает buf, интерпретируется как скаляр размера itemsize. В этом случае оба shape и strides равны NULL.

Если strides равно NULL, то массив интерпретируется как стандартный n-мерный C-массив. В противном случае потребитель должен получить доступ к n-мерному массиву следующим образом:

ptr = (char *)buf + indices[0] * strides[0] + ... + indices[n-1] * strides[n-1];
item = *((typeof(item) *)ptr);

Как отмечалось выше, buf может указывать на любое место в реальном блоке памяти. С помощью этой функции экспортер может проверить валидность буфера:

def verify_structure(memlen, itemsize, ndim, shape, strides, offset):
    """Verify that the parameters represent a valid array within
       the bounds of the allocated memory:
           char *mem: start of the physical memory block
           memlen: length of the physical memory block
           offset: (char *)buf - mem
    """
    if offset % itemsize:
        return False
    if offset < 0 or offset+itemsize > memlen:
        return False
    if any(v % itemsize for v in strides):
        return False

    if ndim <= 0:
        return ndim == 0 and not shape and not strides
    if 0 in shape:
        return True

    imin = sum(strides[j]*(shape[j]-1) for j in range(ndim)
               if strides[j] <= 0)
    imax = sum(strides[j]*(shape[j]-1) for j in range(ndim)
               if strides[j] > 0)

    return 0 <= offset+imin and offset+imax+itemsize <= memlen

PIL-стиль: форма, строки и подмножества

Помимо обычных элементов, массивы в стиле PIL могут содержать указатели, по которым нужно перейти к следующему элементу в измерении. Например, обычный трехмерный C-массив char v[2][2][3] можно также рассматривать как массив из 2 указателей на 2 двумерных массива: char (*v[2])[2][3]. В представлении подмножеств эти два указателя могут быть встроены в начало buf, указывая на два массива char x[2][3], которые могут быть расположены в любом месте памяти.

Вот функция, которая возвращает указатель на элемент N-мерного массива, на который указывает N-мерный индекс, когда есть и не``NULL`` страйдов и подмножеств:

void *get_item_pointer(int ndim, void *buf, Py_ssize_t *strides,
                       Py_ssize_t *suboffsets, Py_ssize_t *indices) {
    char *pointer = (char*)buf;
    int i;
    for (i = 0; i < ndim; i++) {
        pointer += strides[i] * indices[i];
        if (suboffsets[i] >=0 ) {
            pointer = *((char**)pointer) + suboffsets[i];
        }
    }
    return (void*)pointer;
}