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

Певні об’єкти, доступні в Python, обертають доступ до базового масиву пам’яті або буфера. До таких об’єктів належать вбудовані bytes і bytearray, а також деякі типи розширень, наприклад array.array. Бібліотеки сторонніх розробників можуть визначати власні типи для спеціальних цілей, таких як обробка зображень або числовий аналіз.

У той час як кожен із цих типів має власну семантику, вони поділяють спільну характеристику, що вони підтримуються можливо великим буфером пам’яті. Тоді в деяких ситуаціях бажано отримати доступ до цього буфера безпосередньо й без проміжного копіювання.

Python надає таку можливість на рівні C у формі протоколу буфера. Цей протокол має дві сторони:

  • на стороні виробника тип може експортувати «інтерфейс буфера», який дозволяє об’єктам цього типу надавати інформацію про їхній базовий буфер. Цей інтерфейс описано в розділі Буферні об’єктні структури;

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

Прості об’єкти, такі як bytes і bytearray, надають свій базовий буфер у байт-орієнтованій формі. Можливі інші форми; наприклад, елементи, представлені array.array, можуть мати багатобайтові значення.

An example consumer of the buffer interface is the write() method of file objects: any object that can export a series of bytes through the buffer interface can be written to a file. While write() only needs read-only access to the internal contents of the object passed to it, other methods such as readinto() need write access to the contents of their argument. The buffer interface allows objects to selectively allow or reject exporting of read-write and read-only buffers.

Споживач інтерфейсу буфера може отримати буфер над цільовим об’єктом двома способами:

В обох випадках PyBuffer_Release() потрібно викликати, коли буфер більше не потрібен. Якщо цього не зробити, це може призвести до різноманітних проблем, наприклад до витоку ресурсів.

Буферна структура

Буферні структури (або просто «буфери») корисні як спосіб надати двійкові дані з іншого об’єкта програмісту Python. Їх також можна використовувати як механізм нарізки без копіювання. Використовуючи їх здатність посилатися на блок пам’яті, можна досить легко надати будь-які дані програмісту Python. Пам’ять може бути великим постійним масивом у розширенні C, це може бути необроблений блок пам’яті для маніпуляцій перед передачею в бібліотеку операційної системи, або її можна використовувати для передачі структурованих даних у їх рідному форматі в пам’яті. .

На відміну від більшості типів даних, наданих інтерпретатором Python, буфери не є покажчиками PyObject, а досить простими структурами C. Це дозволяє дуже просто створювати та копіювати їх. Якщо потрібна загальна обгортка навколо буфера, можна створити об’єкт memoryview.

Щоб отримати короткі інструкції щодо написання об’єкта експорту, перегляньте Структури об’єктів буфера. Щоб отримати буфер, перегляньте PyObject_GetBuffer().

Py_buffer
void *buf

Покажчик на початок логічної структури, описаної полями буфера. Це може бути будь-яке розташування в базовому блоці фізичної пам’яті експортера. Наприклад, з негативним strides значення може вказувати на кінець блоку пам’яті.

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

void *obj

A new reference to the exporting object. The reference is owned by the consumer and automatically decremented and set to NULL by PyBuffer_Release(). The field is the equivalent of the return value of any standard C-API function.

Як окремий випадок, для тимчасових буферів, які обернуті PyMemoryView_FromBuffer() або PyBuffer_FillInfo(), це поле має значення NULL. Загалом, експорт об’єктів НЕ ПОВИНЕН використовувати цю схему.

Py_ssize_t len

product(shape) * itemsize. Для безперервних масивів це довжина основного блоку пам’яті. Для несуміжних масивів це довжина, яку мала б логічна структура, якби її було скопійовано до безперервного представлення.

Доступ до ((char *)buf)[0] до ((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

A NUL terminated string in struct module style syntax describing the contents of a single item. If this is NULL, "B" (unsigned bytes) is assumed.

Це поле контролюється прапорцем PyBUF_FORMAT.

int ndim

The number of dimensions the memory represents as an n-dimensional array. If it is 0, buf points to a single item representing a scalar. In this case, shape, strides and suboffsets MUST be NULL.

The macro PyBUF_MAX_NDIM limits the maximum number of dimensions to 64. Exporters MUST respect this limit, consumers of multi-dimensional buffers SHOULD be able to handle up to PyBUF_MAX_NDIM dimensions.

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, що вказує кількість байтів, які потрібно пропустити, щоб перейти до нового елемента в кожному вимірі.

Величина кроку може бути будь-яким цілим числом. Для звичайних масивів кроки зазвичай позитивні, але споживач ПОВИНЕН вміти впоратися з випадком ступені[n] <= 0. Див. complex arrays для отримання додаткової інформації.

Масив strides доступний лише для читання для споживача.

Py_ssize_t *suboffsets

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

Якщо всі підзміщення є від’ємними (тобто не потрібно знімати посилання), тоді це поле має бути NULL (значення за замовчуванням).

Цей тип представлення масиву використовується бібліотекою зображень Python (PIL). Див. complex arrays для отримання додаткової інформації про доступ до елементів такого масиву.

Масив suboffsets доступний лише для читання для споживача.

void *internal

Це для внутрішнього використання об’єктом експорту. Наприклад, це може бути перетворено як ціле число експортером і використано для зберігання прапорів про те, чи потрібно звільняти масиви форми, кроків і підзміщень, коли буфер звільняється. Споживач НЕ ПОВИНЕН змінювати це значення.

Типи запитів на буфер

Буфери зазвичай отримують шляхом надсилання запиту на буфер до об’єкта експорту через PyObject_GetBuffer(). Оскільки складність логічної структури пам’яті може різко змінюватися, споживач використовує аргумент flags, щоб визначити точний тип буфера, який він може обробляти.

All Py_buffer fields are unambiguously defined by the request type.

поля, незалежні від запиту

На наступні поля не впливають прапорці, і їх потрібно завжди заповнювати правильними значеннями: 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

так

так

НУЛЬ

PyBUF_ND

так

НУЛЬ

НУЛЬ

PyBUF_SIMPLE

НУЛЬ

НУЛЬ

НУЛЬ

запити суміжності

C або Fortran contiguity можна запитати явно, з інформацією про крок або без неї. Без інформації про кроки буфер має бути C-суміжним.

запит

форму

кроками

підзміщення

контиг

PyBUF_C_CONTIGUOUS

так

так

НУЛЬ

C

PyBUF_F_CONTIGUOUS

так

так

НУЛЬ

Ф

PyBUF_ANY_CONTIGUOUS

так

так

НУЛЬ

C або F

PyBUF_ND

так

НУЛЬ

НУЛЬ

C

складені запити

Усі можливі запити повністю визначені деякою комбінацією прапорів у попередньому розділі. Для зручності протокол буфера надає часто використовувані комбінації як окремі прапорці.

У наступній таблиці U означає невизначену суміжність. Споживач мав би викликати PyBuffer_IsContiguous(), щоб визначити суміжність.

запит

форму

кроками

підзміщення

контиг

лише для читання

формат

PyBUF_FULL

так

так

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

U

0

так

PyBUF_FULL_RO

так

так

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

U

1 або 0

так

PyBUF_RECORDS

так

так

НУЛЬ

U

0

так

PyBUF_RECORDS_RO

так

так

НУЛЬ

U

1 або 0

так

PyBUF_STRIDED

так

так

НУЛЬ

U

0

НУЛЬ

PyBUF_STRIDED_RO

так

так

НУЛЬ

U

1 або 0

НУЛЬ

PyBUF_CONTIG

так

НУЛЬ

НУЛЬ

C

0

НУЛЬ

PyBUF_CONTIG_RO

так

НУЛЬ

НУЛЬ

C

1 або 0

НУЛЬ

Складні масиви

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-D масиві, на який вказує 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;
}