Protocolo de Buffer

Certos objetos disponíveis em Python envolvem o acesso a um vetor ou buffer de memória subjacente. Esses objetos incluem as bytes e bytearray embutidas, e alguns tipos de extensão como array.array. As bibliotecas de terceiros podem definir seus próprios tipos para fins especiais, como processamento de imagem ou análise numérica.

Embora cada um desses tipos tenha sua própria semântica, eles compartilham a característica comum de serem suportados por um buffer de memória possivelmente grande. É desejável, em algumas situações, acessar esse buffer diretamente e sem cópia intermediária.

Python fornece essa facilidade no nível C sob a forma de protocolo de buffer. Este protocolo tem dois lados:

  • do lado do produtor, um tipo pode exportar uma “interface de buffer” que permite que objetos desse tipo exponham informações sobre o buffer subjacente. Esta interface é descrita na seção Buffer Object Structures;

  • do lado do consumidor, vários meios estão disponíveis para obter o ponteiro para os dados subjacentes de um objeto (por exemplo, um parâmetro de método).

Objetos simples como bytes e bytearray expõem seu buffer subjacente em uma forma orientada a byte. Outras formas são possíveis; por exemplo, os elementos expostos por uma array.array podem ser valores de vários bytes.

Um exemplo de interface de um consumidor de buffer é o método write() de objetos arquivo: qualquer objeto que possa exportar uma série de bytes por meio da interface de buffer pode ser gravado em um arquivo. Enquanto o write() precisa apenas de acesso de somente leitura ao conteúdo interno do objeto passado, outros métodos, como readinto(), precisam de acesso de somente escrita ao conteúdo interno. A interface de buffer permite que o objetos possam permitir ou rejeitar a exportação para buffers de leitura e escrita ou somente leitura.

Existem duas maneiras para um consumidor da interface de buffer adquirir um buffer em um objeto alvo:

Em ambos os casos, PyBuffer_Release() deve ser chamado quando o buffer não é mais necessário. A falta de tal pode levar a várias questões, tais como vazamentos de recursos.

Estrutura de Buffer

As estruturas de buffer (ou simplesmente “buffers”) são úteis como uma maneira de expor os dados binários de outro objeto para o programador Python. Eles também podem ser usados como um mecanismo de cópia silenciosa. Usando sua capacidade de fazer referência a um bloco de memória, é possível expor facilmente qualquer dado ao programador Python. A memória pode ser uma matriz grande e constante em uma extensão C, pode ser um bloco bruto de memória para manipulação antes de passar para uma biblioteca do sistema operacional, ou pode ser usado para transmitir dados estruturados no formato nativo e formato de memória.

Ao contrário da maioria dos tipos de dados expostos pelo interpretador Python, os buffers não são ponteiros PyObject mas sim estruturas C simples. Isso permite que eles sejam criados e copiados de forma muito simples. Quando um invólucro genérico em torno de um buffer é necessário, um objeto memoryview pode ser criado.

Para obter instruções curtas sobre como escrever um objeto exportador, consulte Buffer Object Structures. Para obter um buffer, veja PyObject_GetBuffer().

type Py_buffer
Parte da ABI Estável (incluindo todos os membros) desde a versão 3.11.
void *buf

Um ponteiro para o início da estrutura lógica descrita pelos campos do buffer. Este pode ser qualquer local dentro do bloco de memória física subjacente do exportador. Por exemplo, com negativo strides o valor pode apontar para o final do bloco de memória.

Para vetores contíguos, o valor aponta para o início do bloco de memória.

PyObject *obj

Uma nova referência ao objeto sendo exporta. A referência pertence ao consumidor e é automaticamente liberada (por exemplo, a contagem de referências é decrementada) e é atribuída para NULL por PyBuffer_Release(). O campo é equivalmente ao valor de retorno de qualquer função do padrão C-API.

Como um caso especial, para buffers temporários que são encapsulados por PyMemoryView_FromBuffer() ou PyBuffer_FillInfo() esse campo é NULL. Em geral, objetos exportadores NÃO DEVEM usar esse esquema.

Py_ssize_t len

product(shape) * itemsize. Para matrizes contíguas, este é o comprimento do bloco de memória subjacente. Para matrizes não contíguas, é o comprimento que a estrutura lógica teria se fosse copiado para uma representação contígua.

Acessando ((char *)buf)[0] up to ((char *)buf)[len-1] só é válido se o buffer tiver sido obtido por uma solicitação que garanta a contiguidade. Na maioria dos casos, esse pedido será PyBUF_SIMPLE ou PyBUF_WRITABLE.

int readonly

Um indicador de se o buffer é somente leitura. Este campo é controlado pelo sinalizador PyBUF_WRITABLE.

Py_ssize_t itemsize

O tamanho do item em bytes de um único elemento. O mesmo que o valor de struct.calcsize() chamado em valores não NULL de format.

Exceção importante: Se um consumidor requisita um buffer sem sinalizador PyBUF_FORMAT, format será definido como NULL, mas itemsize ainda terá seu valor para o formato original.

Se shape está presente, a igualdade product(shape) * itemsize == len ainda é válida e o usuário pode usar itemsize para navegar o buffer.

Se shape é NULL como resultado de uma PyBUF_SIMPLE ou uma requisição PyBUF_WRITABLE, o consumidor deve ignorar itemsize e assumir itemsize == 1.

const char *format

Uma string terminada por NUL no estilo de sintaxe de módulo struct descrevendo os conteúdos de um único item. Se isso é NULL, "B" (unsigned bytes) é assumido.

Este campo é controlado pelo sinalizador PyBUF_FORMAT.

int ndim

O número de dimensões de memória representado como um array n-dimensional. Se for 0, buf aponta para um único elemento representando um escalar. Neste caso, shape, strides e suboffsets DEVEM ser NULL. O número máximo de dimensões é dado por PyBUF_MAX_NDIM.

Py_ssize_t *shape

Uma matriz de Py_ssize_t do comprimento ndim indicando a forma da memória como uma matriz n-dimensional. Observe que a forma shape[0] * ... * shape[ndim-1] * itemsize DEVE ser igual a len.

Os valores da forma são restritos a shape[n] >= 0. The case shape[n] == 0 requer atenção especial. Veja complex arrays para mais informações.

A forma de acesso a matriz é de somente leitura para o usuário.

Py_ssize_t *strides

Um vetor de Py_ssize_t de comprimento ndim dando o número de bytes para saltar para obter um novo elemento em cada dimensão.

Os valores de Stride podem ser qualquer número inteiro. Para arrays regulares, os passos são geralmente positivos, mas um consumidor DEVE ser capaz de lidar com o caso strides[n] <= 0. Veja complex arrays para mais informações.

A matriz de passos é somente leitura para o consumidor.

Py_ssize_t *suboffsets

Uma matriz de Py_ssize_t de comprimento ndim. Se suboffsets[n] >= 0, os valores armazenados ao longo da n-ésima dimensão são ponteiros e o valor suboffset determina quantos bytes para adicionar a cada ponteiro após desreferenciar. Um valor de suboffset que é negativo indica que não deve ocorrer desreferenciação (caminhando em um bloco de memória contíguo).

Se todos os subconjuntos forem negativos (ou seja, não é necessário fazer referência), então este campo deve ser NULL (o valor padrão).

Esse tipo de representação de matriz é usado pela Python Imaging Library (PIL). Veja complex arrays para obter mais informações sobre como acessar elementos dessa matriz.a matriz.

A matriz de subconjuntos é somente leitura para o consumidor.

void *internal

Isso é para uso interno pelo objeto exportador. Por exemplo, isso pode ser re-moldado como um número inteiro pelo exportador e usado para armazenar bandeiras sobre se os conjuntos de forma, passos e suboffsets devem ou não ser liberados quando o buffer é liberado. O consumidor NÃO DEVE alterar esse valor.

Constantes:

PyBUF_MAX_NDIM

O número máximo de dimensões que a memória representa. Exportadores DEVEM respeitar esse limite, consumidores de buffers multi-dimensionais DEVEM ser capazes de liader com até PyBUF_MAX_NDIM dimensões. Atualmente definido como 64.

Tipos de solicitação do buffer

Os buffers geralmente são obtidos enviando uma solicitação de buffer para um objeto exportador via PyObject_GetBuffer(). Uma vez que a complexidade da estrutura lógica da memória pode variar drasticamente, o consumidor usa o argumento flags para especificar o tipo de buffer exato que pode manipular.

Todos os campos Py_buffer são definidos de forma não-ambígua pelo tipo de requisição.

campos independentes do pedido

Os seguintes campos não são influenciados por flags e devem sempre ser preenchidos com os valores corretos: obj, buf, len, itemsize, ndim.

apenas em formato

PyBUF_WRITABLE

Controla o campo readonly. Se configurado, o exportador DEVE fornecer um buffer gravável ou então reportar falha. Caso contrário, o exportador pode fornecer um buffer de somente leitura ou gravável, mas a escolha DEVE ser consistente para todos os consumidores.

PyBUF_FORMAT

Controla o campo format. Se configurado, este campo DEVE ser preenchido corretamente. Caso contrário, este campo DEVE ser NULL.

:PyBUF_WRITABLE pode ser |’d para qualquer um dos sinalizadores na próxima seção. Uma vez que PyBUF_WRITABLE é definido como 0, PyBUF_WRITABLE pode ser usado como uma bandeira autônoma para solicitar um buffer simples gravável.

PyBUF_FORMAT pode ser |’d para qualquer um dos sinalizadores, exceto PyBUF_SIMPLE. O último já implica o formato B (bytes não assinados).

forma, avanços, suboffsets

As bandeiras que controlam a estrutura lógica da memória estão listadas em ordem decrescente de complexidade. Observe que cada bandeira contém todos os bits das bandeiras abaixo.

Solicitação

Forma

Avanços

subconjuntos

PyBUF_INDIRECT

sim

sim

se necessário

PyBUF_STRIDES

sim

sim

NULL

PyBUF_ND

sim

NULL

NULL

PyBUF_SIMPLE

NULL

NULL

NULL

requisições contíguas

contiguity do C ou Fortran podem ser explicitamente solicitadas, com ou sem informação de avanço. Sem informação de avanço, o buffer deve ser C-contíguo.

Solicitação

Forma

Avanços

subconjuntos

contig

PyBUF_C_CONTIGUOUS

sim

sim

NULL

C

PyBUF_F_CONTIGUOUS

sim

sim

NULL

F

PyBUF_ANY_CONTIGUOUS

sim

sim

NULL

C ou F

PyBUF_ND

sim

NULL

NULL

C

requisições compostas

Todas as requisições possíveis foram completamente definidas por alguma combinação dos sinalizadores na seção anterior. Por conveniência, o protocolo do buffer fornece combinações frequentemente utilizadas como sinalizadores únicos.

Na seguinte tabela U significa contiguidade indefinida. O consumidor deve chamar PyBuffer_IsContiguous() para determinar a contiguidade.

Solicitação

Forma

Avanços

subconjuntos

contig

readonly

formato

PyBUF_FULL

sim

sim

se necessário

U

0

sim

PyBUF_FULL_RO

sim

sim

se necessário

U

1 ou 0

sim

PyBUF_RECORDS

sim

sim

NULL

U

0

sim

PyBUF_RECORDS_RO

sim

sim

NULL

U

1 ou 0

sim

PyBUF_STRIDED

sim

sim

NULL

U

0

NULL

PyBUF_STRIDED_RO

sim

sim

NULL

U

1 ou 0

NULL

PyBUF_CONTIG

sim

NULL

NULL

C

0

NULL

PyBUF_CONTIG_RO

sim

NULL

NULL

C

1 ou 0

NULL

Vetores Complexos

Estilo NumPy: forma e avanços

A estrutura lógica de vetores do estilo NumPy é definida por itemsize, ndim, shape e strides.

Se ndim == 0, a localização da memória apontada para buf é interpretada como um escalar de tamanho itemsize. Nesse caso, ambos shape e strides são NULL.

Se strides é NULL, o vetor é interpretado como um vetor C n-dimensional padrão. Caso contrário, o consumidor deve acessar um vetor n-dimensional como a seguir:

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

Como notado acima, buf pode apontar para qualquer localização dentro do bloco de memória em si. Um exportador pode verificar a validade de um buffer com essa função:

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

Estilo-PIL: forma, avanços e suboffsets

Além dos itens normais, uma matriz em estilo PIL pode conter ponteiros que devem ser seguidos para se obter o próximo elemento em uma dimensão. Por exemplo, a matriz tridimensional em C char v[2][2][3] também pode ser vista como um vetor de 2 ponteiros para duas matrizes bidimensionais: char (*v[2])[2][3]. Na representação por suboffsets, esses dois ponteiros podem ser embutidos no início de buf, apontando para duas matrizes char x[2][3] que podem estar localizadas em qualquer lugar na memória.

Esta é uma função que retorna um ponteiro para o elemento em uma matriz N-D apontada por um índice N-dimensional onde existem ambos passos e subconjuntos não-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;
}