Протокол буфера
Определённые объекты в Python обеспечивают доступ к массиву памяти или буферу. К таким объектам относятся встроенные bytes и bytearray, а также некоторые типы расширений, такие как array.array. Третьей стороны библиотеки могут определять свои типы для специальных целей, таких как обработка изображений или числовой анализ.
Хотя у каждого из этих типов есть свои семантика, они обладают общим свойством — они опираются на, возможно, большой буфер памяти. В некоторых ситуациях желательно получить прямой доступ к этому буферу без промежуточных копирований.
Python предоставляет такую возможность на уровне C в виде протокола буфера. Этот протокол имеет две стороны:
- со стороны производителя, тип может экспортировать “интерфейс буфера”, который позволяет объектам этого типа экспортировать информацию о базовом буфере. Этот интерфейс описан в разделе Структуры объекта буфера;
- со стороны потребителя, доступно несколько способов получения указателя на исходные данные объекта (например, параметр метода).
Простые объекты, такие как bytes и bytearray, экспортируют свой базовый буфер в байтовом формате. Возможны и другие форматы; например, элементы, экспортируемые объектом array.array, могут быть многобайтовыми значениями.
Пример потребителя интерфейса буфера — метод write() объектов файлов: любой объект, который может экспортировать серию байтов через интерфейс буфера, может быть записан в файл. Хотя write() требует только только чтения содержимого переданного ему объекта, другие методы, такие как readinto(), нуждаются в записи в содержимое своего аргумента. Интерфейс буфера позволяет объектам выборочно разрешать или запрещать экспорт буферов для чтения и для чтения-записи.
Существует два способа для потребителя интерфейса буфера получить буфер над целевым объектом:
- вызов
PyObject_GetBuffer()с правильными параметрами; - вызов
PyArg_ParseTuple()(или одного из его аналогов) с одним изy*,w*илиs*форматов.
В обоих случаях необходимо вызвать PyBuffer_Release(), когда буфер больше не нужен. Отсутствие этого вызова может привести к различным проблемам, таким как утечка ресурсов.
Структура буфера
Структуры буферов (или просто «буферы») полезны как способ экспонирования двоичных данных из другого объекта для программиста Python. Они также могут использоваться как механизм нулевого копирования для срезов. Используя их способность ссылаться на блок памяти, можно довольно легко экспонировать любые данные для программиста Python. Память может быть большим постоянным массивом в расширении C, это может быть сырой блок памяти для манипулирования перед передачей в библиотеку операционной системы или он может использоваться для передачи структурированных данных в его собственном формате в памяти.
В отличие от большинства типов данных, экспонируемых интерпретатором Python, буферы не являются указателями PyObject, а скорее простыми структурами C. Это позволяет создавать и копировать их очень просто. Когда нужен обобщенный обертка вокруг буфера, можно создать объект memoryview.
Для кратких инструкций по написанию экспортируемого объекта см. Структуры объекта буфера. Для получения буфера см. PyObject_GetBuffer().
-
Py_buffer -
-
void *buf -
Указатель на начало логической структуры, описанной полями буфера. Это может быть любое место в базовом физическом блоке памяти экспортера. Например, с отрицательными
stridesзначение может указывать на конец блока памяти.Для смежных массивов значение указывает на начало блока памяти.
-
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(), вызываемого для значений безNULLformat.Важное исключение: Если потребитель запрашивает буфер без флага
PyBUF_FORMAT,formatбудет установлен вNULL, ноitemsizeвсё ещё сохранит значение для исходного формата.Если
shapeприсутствует, равенствоproduct(shape) * itemsize == lenвсё ещё справедливо, и потребитель может использоватьitemsizeдля навигации по буферу.Если
shapeприсутствует как результат запросаPyBUF_SIMPLEилиPyBUF_WRITABLE, потребитель должен игнорироватьitemsizeи предположитьitemsize == 1.
-
const char *format -
Строка с нулевым окончанием в формате модуля
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требует особого внимания. Смотрите сложные массивы для получения дополнительной информации.Массив формы для потребителя является только для чтения.
-
Py_ssize_t *strides -
Массив
Py_ssize_tдлинойndim, который указывает количество байтов, которое нужно пропустить, чтобы получить новый элемент в каждом измерении.Значения шагов могут быть любыми целыми числами. Для обычных массивов шаги обычно положительные, но потребитель ДОЛЖЕН уметь обрабатывать случай
strides[n] <= 0. Смотрите сложные массивы для получения дополнительной информации.Массив шагов для потребителя является только для чтения.
-
Py_ssize_t *suboffsets -
Массив
Py_ssize_tдлинойndim. Еслиsuboffsets[n] >= 0, значения, хранящиеся вдоль n-го измерения, являются указателями, и значение suboffset определяет, сколько байтов нужно добавить к каждому указателю после разыменования. Значение suboffset, которое отрицательно, указывает, что разыменование не должно происходить (шаги по блоку смежной памяти).Если все suboffset отрицательны (то есть разыменования не требуется), то это поле ДОЛЖНО быть
NULL(значение по умолчанию).Этот тип представления массива используется библиотекой Python Imaging Library (PIL). См. сложные массивы для получения дополнительной информации о том, как обращаться к элементам такого массива.
Массив suboffsets для потребителя является только для чтения.
-
void *internal -
Используется внутри экспортирующего объекта. Например, экспортер может перевести это в целое число и использовать его для хранения флагов о том, нужно ли освобождать массивы shape, strides и suboffsets при освобождении буфера. Потребитель НЕ ДОЛЖЕН изменять это значение.
-
Типы запросов на буферы
Буферы обычно получаются путём отправки запроса на буфер объекту-экспортеру через PyObject_GetBuffer(). Поскольку сложность логической структуры памяти может сильно варьироваться, потребитель использует аргумент flags для указания точного типа буфера, который он может обработать.
Все поля Py_buffer однозначно определяются типом запроса.
Независимые от запроса поля
Следующие поля не зависят от flags и всегда должны быть заполнены правильными значениями: obj, buf, len, itemsize, ndim.
Только чтение, формат
-
PyBUF_WRITABLE -
Управляет полем
readonly. Если установлено, экспортер ОБЯЗАН предоставить доступный для записи буфер, в противном случае должен сообщить об ошибке. В противном случае экспортер МОЖЕТ предоставить буфер только для чтения или доступный для записи, но этот выбор ОБЯЗАН быть согласованным для всех потребителей.
-
PyBUF_FORMAT -
Управляет полем
format. Если установлено, это поле ОБЯЗАТЕЛЬНО должно быть заполнено корректно. В противном случае это поле ДОЛЖНО бытьNULL.
PyBUF_WRITABLE может быть объединено с любыми из флагов в следующем разделе. Поскольку PyBUF_SIMPLE определён как 0, PyBUF_WRITABLE может использоваться как отдельный флаг для запроса простого буфера для записи.
PyBUF_FORMAT может быть объединено с любыми флагами, кроме PyBUF_SIMPLE. Последний уже подразумевает формат B (беззнаковые байты).
shape, strides, suboffsets
Флаги, которые управляют логической структурой памяти, перечислены в порядке убывания сложности. Обратите внимание, что каждый флаг содержит все биты нижележащих флагов.
Запрос | shape | strides | suboffsets |
|---|---|---|---|
| да | да | если необходимо |
| да | да | NULL |
| да | NULL | NULL |
| NULL | NULL | NULL |
Запросы на непрерывность
C или Fortran непрерывность может быть явно запрошена с и без информации о шагах. Без информации о шагах буфер должен быть непрерывным по C.
Запрос | shape | strides | suboffsets | contig |
|---|---|---|---|---|
| да | да | NULL | C |
| да | да | NULL | F |
| да | да | NULL | C или F |
да | NULL | NULL | C |
Составные запросы
Все возможные запросы полностью определяются какой-либо комбинацией флагов в предыдущем разделе. Для удобства протокол буферов предоставляет часто используемые комбинации в виде отдельных флагов.
В следующей таблице U обозначает неопределённую непрерывность. Потребитель должен будет вызвать PyBuffer_IsContiguous() для определения непрерывности.
Запрос | shape | strides | suboffsets | contig | только чтение | формат |
|---|---|---|---|---|---|---|
| да | да | если необходимо | U | 0 | да |
| да | да | если необходимо | U | 1 или 0 | да |
| да | да | NULL | U | 0 | да |
| да | да | NULL | U | 1 или 0 | да |
| да | да | NULL | U | 0 | NULL |
| да | да | NULL | U | 1 или 0 | NULL |
| да | NULL | NULL | C | 0 | NULL |
| да | NULL | NULL | C | 1 или 0 | NULL |
Сложные массивы
Стиль NumPy: shape и strides
Логическая структура массивов в стиле 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: shape, strides и suboffsets
В дополнение к обычным элементам, массивы в стиле PIL могут содержать указатели, за которыми необходимо следовать, чтобы получить следующий элемент в измерении. Например, обычный трёхмерный массив C char v[2][2][3] также может быть представлен как массив из 2 указателей на 2 двухмерных массива: char (*v[2])[2][3]. В представлении suboffsets эти два указателя могут быть встроены в начало buf, указывая на два char x[2][3] массива, которые могут быть расположены где угодно в памяти.
Вот функция, которая возвращает указатель на элемент в n-мерном массиве, на который указывает n-мерный индекс, когда есть как не-NULL шаги, так и suboffsets:
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;
}
© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.9/c-api/buffer.html