Buffer-Protokoll

Bestimmte Objekte in Python bieten Zugriff auf ein zugrunde liegendes Speicherarray oder einen Puffer. Zu diesen Objekten gehören die integrierten Typen bytes und bytearray sowie einige Erweiterungstypen wie array.array. Drittanbieterbibliotheken können eigene Typen für spezielle Zwecke definieren, z. B. für die Bildverarbeitung oder numerische Analysen.

Während jeder dieser Typen seine eigene Semantik hat, teilen sie die gemeinsame Eigenschaft, dass sie durch einen möglicherweise großen Speicherpuffer gesichert sind. Es ist daher in einigen Situationen wünschenswert, direkt und ohne Zwischenkopien auf diesen Puffer zuzugreifen.

Python stellt diese Funktionalität auf C- und Python-Ebene in Form des Buffer-Protokolls bereit. Dieses Protokoll hat zwei Seiten

  • Auf der Produzentenseite kann ein Typ eine "Buffer-Schnittstelle" exportieren, die es Objekten dieses Typs ermöglicht, Informationen über ihren zugrunde liegenden Puffer preiszugeben. Diese Schnittstelle wird im Abschnitt Buffer-Objektstrukturen beschrieben; für Python siehe Emulation von Puffer-Typen.

  • Auf der Konsumentenseite stehen mehrere Möglichkeiten zur Verfügung, um einen Zeiger auf die rohen zugrunde liegenden Daten eines Objekts zu erhalten (z. B. als Methodenparameter). Für Python siehe memoryview.

Einfache Objekte wie bytes und bytearray geben ihren zugrunde liegenden Puffer in byte-orientierter Form aus. Andere Formen sind möglich; zum Beispiel können die von einem array.array exponierten Elemente mehrbyte-Werte sein.

Ein Beispiel für einen Konsumenten der Buffer-Schnittstelle ist die write()-Methode von Datei-Objekten: Jedes Objekt, das eine Byte-Sequenz über die Buffer-Schnittstelle exportieren kann, kann in eine Datei geschrieben werden. Während write() nur Lesezugriff auf den internen Inhalt des übergebenen Objekts benötigt, benötigen andere Methoden wie readinto() Schreibzugriff auf den Inhalt ihres Arguments. Die Buffer-Schnittstelle ermöglicht es Objekten, den Export von Lese-/Schreib- und schreibgeschützten Puffern selektiv zu erlauben oder abzulehnen.

Es gibt zwei Möglichkeiten für einen Konsumenten der Buffer-Schnittstelle, einen Puffer über ein Zielobjekt zu erwerben

In beiden Fällen muss PyBuffer_Release() aufgerufen werden, wenn der Puffer nicht mehr benötigt wird. Andernfalls kann es zu verschiedenen Problemen wie Ressourcenlecks kommen.

Hinzugefügt in Version 3.12: Das Buffer-Protokoll ist jetzt in Python zugänglich, siehe Emulation von Puffer-Typen und memoryview.

Buffer-Struktur

Buffer-Strukturen (oder einfach "Puffer") sind nützlich, um Binärdaten aus einem anderen Objekt für den Python-Programmierer bereitzustellen. Sie können auch als Mechanismus für Zero-Copy-Slicing verwendet werden. Durch ihre Fähigkeit, einen Speicherblock zu referenzieren, ist es möglich, beliebige Daten dem Python-Programmierer sehr einfach zugänglich zu machen. Der Speicher kann ein großes, konstantes Array in einer C-Erweiterung sein, ein roher Speicherblock zur Manipulation, bevor er an eine Betriebssystembibliothek übergeben wird, oder er kann zum Weitergeben strukturierter Daten in seinem nativen In-Memory-Format verwendet werden.

Im Gegensatz zu den meisten Datentypen, die vom Python-Interpreter bereitgestellt werden, sind Puffer keine PyObject-Zeiger, sondern einfache C-Strukturen. Dies ermöglicht ihre einfache Erstellung und Kopie. Wenn ein generischer Wrapper um einen Puffer benötigt wird, kann ein memoryview-Objekt erstellt werden.

Für kurze Anweisungen, wie man ein exportierendes Objekt schreibt, siehe Buffer-Objektstrukturen. Zum Erhalten eines Puffers siehe PyObject_GetBuffer().

type Py_buffer
Teil der Stable ABI (einschließlich aller Member) seit Version 3.11.
void *buf

Ein Zeiger auf den Anfang der logischen Struktur, die durch die Pufferfelder beschrieben wird. Dies kann jede Position innerhalb des zugrunde liegenden physischen Speicherblocks des Exporteurs sein. Zum Beispiel kann bei negativen strides der Wert auf das Ende des Speicherblocks zeigen.

Für kontinuierliche Arrays zeigt der Wert auf den Anfang des Speicherblocks.

PyObject *obj

Eine neue Referenz auf das exportierende Objekt. Die Referenz gehört dem Konsumenten und wird automatisch freigegeben (d. h. der Referenzzähler wird dekrementiert) und auf NULL gesetzt von PyBuffer_Release(). Das Feld ist das Äquivalent zum Rückgabewert jeder Standard-C-API-Funktion.

Als Sonderfall sind für temporäre Puffer, die von PyMemoryView_FromBuffer() oder PyBuffer_FillInfo() umwickelt werden, die Felder NULL. Im Allgemeinen dürfen exportierende Objekte dieses Schema NICHT verwenden.

Py_ssize_t len

product(shape) * itemsize. Für kontinuierliche Arrays ist dies die Länge des zugrunde liegenden Speicherblocks. Für nicht-kontinuierliche Arrays ist es die Länge, die die logische Struktur hätte, wenn sie in eine kontinuierliche Darstellung kopiert würde.

Der Zugriff auf ((char *)buf)[0] bis ((char *)buf)[len-1] ist nur gültig, wenn der Puffer durch eine Anfrage erhalten wurde, die Kontinuität garantiert. In den meisten Fällen ist eine solche Anfrage PyBUF_SIMPLE oder PyBUF_WRITABLE.

int readonly

Eine Angabe, ob der Puffer schreibgeschützt ist. Dieses Feld wird durch das Flag PyBUF_WRITABLE gesteuert.

Py_ssize_t itemsize

Elementgröße in Bytes eines einzelnen Elements. Gleicht dem Wert von struct.calcsize(), aufgerufen auf Nicht-NULL format-Werten.

Wichtige Ausnahme: Wenn ein Konsument einen Puffer ohne das Flag PyBUF_FORMAT anfordert, wird format auf NULL gesetzt, aber itemsize hat immer noch den Wert für das ursprüngliche Format.

Wenn shape vorhanden ist, gilt die Gleichheit product(shape) * itemsize == len weiterhin und der Konsument kann itemsize verwenden, um den Puffer zu navigieren.

Wenn shape aufgrund einer Anforderung von PyBUF_SIMPLE oder PyBUF_WRITABLE NULL ist, muss der Konsument itemsize ignorieren und annehmen, dass itemsize == 1.

char *format

Ein NULL-terminierter String in der Syntax des struct-Moduls, der den Inhalt eines einzelnen Elements beschreibt. Wenn dies NULL ist, wird "B" (vorzeichenlose Bytes) angenommen.

Dieses Feld wird durch das Flag PyBUF_FORMAT gesteuert.

int ndim

Die Anzahl der Dimensionen, die der Speicher als n-dimensionales Array darstellt. Wenn es 0 ist, zeigt buf auf ein einzelnes Element, das einen Skalar darstellt. In diesem Fall MÜSSEN shape, strides und suboffsets NULL sein. Die maximale Anzahl von Dimensionen wird durch PyBUF_MAX_NDIM angegeben.

Py_ssize_t *shape

Ein Array von Py_ssize_t der Länge ndim, das die Form des Speichers als n-dimensionales Array angibt. Beachten Sie, dass shape[0] * ... * shape[ndim-1] * itemsize gleich len sein MUSS.

Formwerte sind auf shape[n] >= 0 beschränkt. Der Fall shape[n] == 0 erfordert besondere Aufmerksamkeit. Weitere Informationen finden Sie unter komplexe Arrays.

Das Formular-Array ist für den Konsumenten schreibgeschützt.

Py_ssize_t *strides

Ein Array von Py_ssize_t der Länge ndim, das die Anzahl der Bytes angibt, die übersprungen werden müssen, um zu einem neuen Element in jeder Dimension zu gelangen.

Stride-Werte können beliebige ganze Zahlen sein. Für reguläre Arrays sind Stride-Werte normalerweise positiv, aber ein Konsument MUSS den Fall strides[n] <= 0 handhaben können. Weitere Informationen finden Sie unter komplexe Arrays.

Das Stride-Array ist für den Konsumenten schreibgeschützt.

Py_ssize_t *suboffsets

Ein Array von Py_ssize_t der Länge ndim. Wenn suboffsets[n] >= 0, sind die entlang der n-ten Dimension gespeicherten Werte Zeiger und der Suboffset-Wert gibt an, wie viele Bytes nach der Dereferenzierung zu jedem Zeiger addiert werden müssen. Ein negativer Suboffset-Wert bedeutet, dass keine Dereferenzierung erfolgen soll (Striding in einem kontinuierlichen Speicherblock).

Wenn alle Suboffsets negativ sind (d. h. keine Dereferenzierung erforderlich ist), muss dieses Feld NULL sein (der Standardwert).

Diese Art der Array-Darstellung wird von der Python Imaging Library (PIL) verwendet. Weitere Informationen zum Zugriff auf Elemente eines solchen Arrays finden Sie unter komplexe Arrays.

Das Suboffsets-Array ist für den Konsumenten schreibgeschützt.

void *internal

Dies ist zur internen Verwendung durch das exportierende Objekt bestimmt. Zum Beispiel könnte dies vom Exporteur als Integer neu gecastet und zur Speicherung von Flags verwendet werden, die angeben, ob die Arrays shape, strides und suboffsets freigegeben werden müssen, wenn der Puffer freigegeben wird. Der Konsument darf diesen Wert NICHT ändern.

Konstanten

PyBUF_MAX_NDIM

Die maximale Anzahl von Dimensionen, die der Speicher darstellt. Exporteure MÜSSEN dieses Limit einhalten, Konsumenten von mehrdimensionalen Puffern SOLLTEN bis zu PyBUF_MAX_NDIM Dimensionen verarbeiten können. Derzeit auf 64 gesetzt.

Buffer-Anforderungstypen

Puffer werden normalerweise erhalten, indem eine Pufferanforderung über PyObject_GetBuffer() an ein exportierendes Objekt gesendet wird. Da die Komplexität der logischen Struktur des Speichers drastisch variieren kann, verwendet der Konsument das Argument flags, um den genauen Puffer-Typ anzugeben, den er verarbeiten kann.

Alle Felder von Py_buffer werden eindeutig durch den Anforderungstyp definiert.

Anforderungsunabhängige Felder

Die folgenden Felder werden nicht von flags beeinflusst und müssen immer mit den korrekten Werten gefüllt werden: obj, buf, len, itemsize, ndim.

readonly, format

PyBUF_WRITABLE

Steuert das Feld readonly. Wenn gesetzt, MUSS der Exporteur einen beschreibbaren Puffer bereitstellen oder einen Fehler melden. Andernfalls KANN der Exporteur entweder einen schreibgeschützten oder einen beschreibbaren Puffer bereitstellen, aber die Wahl MUSS für alle Konsumenten konsistent sein. Zum Beispiel kann PyBUF_SIMPLE | PyBUF_WRITABLE verwendet werden, um einen einfachen beschreibbaren Puffer anzufordern.

PyBUF_FORMAT

Steuert das Feld format. Wenn gesetzt, MUSS dieses Feld korrekt gefüllt werden. Andernfalls MUSS dieses Feld NULL sein.

PyBUF_WRITABLE kann mit jedem der Flags im nächsten Abschnitt verknüpft werden. Da PyBUF_SIMPLE als 0 definiert ist, kann PyBUF_WRITABLE als eigenständiges Flag verwendet werden, um einen einfachen beschreibbaren Puffer anzufordern.

PyBUF_FORMAT muss mit jedem der Flags außer PyBUF_SIMPLE verknüpft werden, da letzteres bereits das Format B (vorzeichenlose Bytes) impliziert. PyBUF_FORMAT kann nicht allein verwendet werden.

shape, strides, suboffsets

Die Flags, die die logische Struktur des Speichers steuern, sind in absteigender Reihenfolge der Komplexität aufgeführt. Beachten Sie, dass jedes Flag alle Bits der darunter liegenden Flags enthält.

Anforderung

shape

strides

suboffsets

PyBUF_INDIRECT

ja

ja

falls erforderlich

PyBUF_STRIDES

ja

ja

NULL

PyBUF_ND

ja

NULL

NULL

PyBUF_SIMPLE

NULL

NULL

NULL

Kontinuitätsanforderungen

C- oder Fortran-Kontinuität kann explizit angefordert werden, mit und ohne Stride-Informationen. Ohne Stride-Informationen muss der Puffer C-kontinuierlich sein.

Anforderung

shape

strides

suboffsets

contig

PyBUF_C_CONTIGUOUS

ja

ja

NULL

C

PyBUF_F_CONTIGUOUS

ja

ja

NULL

F

PyBUF_ANY_CONTIGUOUS

ja

ja

NULL

C oder F

PyBUF_ND

ja

NULL

NULL

C

Verbundanforderungen

Alle möglichen Anforderungen sind vollständig durch eine Kombination der Flags im vorherigen Abschnitt definiert. Zur Vereinfachung stellt das Buffer-Protokoll häufig verwendete Kombinationen als einzelne Flags bereit.

In der folgenden Tabelle steht U für undefinierte Kontinuität. Der Konsument müsste PyBuffer_IsContiguous() aufrufen, um die Kontinuität zu ermitteln.

Anforderung

shape

strides

suboffsets

contig

readonly

format

PyBUF_FULL

ja

ja

falls erforderlich

U

0

ja

PyBUF_FULL_RO

ja

ja

falls erforderlich

U

1 oder 0

ja

PyBUF_RECORDS

ja

ja

NULL

U

0

ja

PyBUF_RECORDS_RO

ja

ja

NULL

U

1 oder 0

ja

PyBUF_STRIDED

ja

ja

NULL

U

0

NULL

PyBUF_STRIDED_RO

ja

ja

NULL

U

1 oder 0

NULL

PyBUF_CONTIG

ja

NULL

NULL

C

0

NULL

PyBUF_CONTIG_RO

ja

NULL

NULL

C

1 oder 0

NULL

Komplexe Arrays

NumPy-Stil: Shape und Strides

Die logische Struktur von Arrays im NumPy-Stil wird durch itemsize, ndim, shape und strides definiert.

Wenn ndim == 0, wird die von buf angezeigte Speicheradresse als Skalar der Größe itemsize interpretiert. In diesem Fall sind sowohl shape als auch strides NULL.

Wenn strides NULL ist, wird das Array als Standard-n-dimensionale C-Array interpretiert. Andernfalls muss der Verbraucher wie folgt auf ein n-dimensionales Array zugreifen

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

Wie oben erwähnt, kann buf auf jede beliebige Stelle innerhalb des tatsächlichen Speicherblocks zeigen. Ein Exporteur kann die Gültigkeit eines Puffers mit dieser Funktion überprüfen

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-Stil: Shape, Strides und Suboffsets

Zusätzlich zu den regulären Elementen können PIL-artige Arrays Zeiger enthalten, denen gefolgt werden muss, um zum nächsten Element einer Dimension zu gelangen. Zum Beispiel kann das reguläre dreidimensionale C-Array char v[2][2][3] auch als ein Array von 2 Zeigern auf 2 zweidimensionale Arrays angesehen werden: char (*v[2])[2][3]. In der Suboffset-Darstellung können diese beiden Zeiger am Anfang von buf eingebettet sein und auf zwei char x[2][3]-Arrays zeigen, die sich irgendwo im Speicher befinden können.

Hier ist eine Funktion, die einen Zeiger auf das Element in einem N-D-Array zurückgibt, auf das von einem n-dimensionalen Index gezeigt wird, wenn sowohl nicht-NULL-Strides als auch Suboffsets vorhanden sind

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;
}