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

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

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

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

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

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

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

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

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

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

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

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

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

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

type Py_buffer
void *buf

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

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

void *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.

const char *format

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

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

int ndim

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

Макрос PyBUF_MAX_NDIM ограничивает максимальное количество измерений до 64. Экспортеры ДОЛЖНЫ соблюдать это ограничение, потребители многомерных буферов ДОЛЖНЫ иметь возможность обрабатывать до 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 быть освобождены при освобождении буфера. Потребитель НЕ ДОЛЖЕН изменять это значение.

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

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

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

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

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

только для чтения, формат

PyBUF_WRITABLE

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

PyBUF_FORMAT

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

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

PyBUF_FORMAT может быть |“d к любому из флагов, кроме 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;
}
Вернуться на верх