버퍼 프로토콜

파이썬에서 사용할 수 있는 어떤 객체는 하부 메모리 배열 또는 버퍼에 대한 액세스를 감쌉니다. 이러한 객체에는 내장 bytes 와 bytearray, 그리고 array.array와 같은 일부 확장형이 포함됩니다. 제삼자 라이브러리도 이미지 처리나 수치 해석과 같은 특수한 용도로 자체 형을 정의할 수 있습니다.

이러한 형은 각각 고유의 의미가 있지만, (아마도) 큰 메모리 버퍼에 의해 뒷받침되는 공통된 특징을 공유합니다. 어떤 상황에서는 중간 복사 없이 직접 버퍼에 액세스하는 것이 바람직합니다.

파이썬은 C 수준에서 버퍼 프로토콜 형식으로 이러한 기능을 제공합니다. 이 프로토콜에는 두 가지 측면이 있습니다:

  • 생산자 측에서는, 형이 “버퍼 인터페이스”를 내보낼 수 있는데, 그 형의 객체가 하부 버퍼의 정보를 노출할 수 있게 합니다. 이 인터페이스는 버퍼 객체 구조체 절에서 설명됩니다.

  • 소비자 측에서는, 객체의 원시 하부 데이터에 대한 포인터를 얻기 위해 여러 가지 방법을 사용할 수 있습니다 (예를 들어 메서드 매개 변수).

bytes 와 bytearray와 같은 간단한 객체는 하부 버퍼를 바이트 지향 형식으로 노출합니다. 다른 형태도 가능합니다; 예를 들어, array.array에 의해 노출되는 요소는 멀티 바이트 값이 될 수 있습니다.

버퍼 인터페이스의 소비자 예는 파일 객체의 write() 메서드입니다: 버퍼 인터페이스를 통해 일련의 바이트를 내보낼 수 있는 모든 객체는 파일에 기록될 수 있습니다. write()가 전달된 객체의 내부 내용에 대한 읽기 전용 액세스만 필요하지만, readinto()와 같은 다른 메서드는 인자의 내용에 쓰기 액세스가 필요합니다. 버퍼 인터페이스는 객체가 읽기-쓰기와 읽기 전용 버퍼를 선택적으로 허용하거나 거부할 수 있도록 합니다.

버퍼 인터페이스의 소비자가 대상 객체에 대해 버퍼를 얻는 방법에는 두 가지가 있습니다:

두 경우 모두, 버퍼가 더는 필요하지 않으면 PyBuffer_Release()를 호출해야 합니다. 그렇게 하지 않으면 자원 누수와 같은 다양한 문제가 발생할 수 있습니다.

버퍼 구조체

버퍼 구조체(또는 단순히 “버퍼”)는 다른 객체의 바이너리 데이터를 파이썬 프로그래머에게 노출하는 방법으로 유용합니다. 또한, 복사 없는(zero-copy) 슬라이싱 메커니즘으로 사용할 수 있습니다. 메모리 블록을 참조하는 능력을 사용해서, 임의의 데이터를 파이썬 프로그래머에게 아주 쉽게 노출할 수 있습니다. 메모리는 C 확장의 큰 상수 배열일 수 있으며, 운영 체제 라이브러리로 전달되기 전에 조작하기 위한 원시 메모리 블록일 수도 있고, 네이티브 인 메모리(in-memory) 형식으로 구조화된 데이터를 전달하는 데 사용될 수도 있습니다.

파이썬 인터프리터가 노출하는 대부분의 데이터형과 달리, 버퍼는 PyObject 포인터가 아니라 단순한 C 구조체입니다. 이를 통해 매우 간단하게 만들고 복사할 수 있습니다. 버퍼를 감싸는 일반 래퍼가 필요할 때는, 메모리 뷰 객체를 만들 수 있습니다.

제공하는(exporting) 객체를 작성하는 간단한 지침은 버퍼 객체 구조체를 참조하십시오. 버퍼를 얻으려면, PyObject_GetBuffer()를 참조하십시오.

Py_buffer
void *buf

버퍼 필드에 의해 기술된 논리적 구조의 시작을 가리키는 포인터. 이것은 제공자(exporter)의 하부 물리적 메모리 블록 내의 모든 위치일 수 있습니다. 예를 들어, 음의 strides를 사용하면 값이 메모리 블록의 끝을 가리킬 수 있습니다.

연속 배열의 경우, 값은 메모리 블록의 시작을 가리킵니다.

void *obj

제공하는(exporting) 객체에 대한 새 참조. 참조는 소비자가 소유하고, PyBuffer_Release()에 의해 자동으로 감소하고 NULL로 설정됩니다. 이 필드는 표준 C-API 함수의 반환 값과 동등합니다.

특수한 경우로, PyMemoryView_FromBuffer() 나 PyBuffer_FillInfo()로 감싸진 임시(temporary) 버퍼의 경우, 이 필드는 NULL입니다. 일반적으로, 제공하는(exporting) 객체는 이 체계를 사용하지 않아야 합니다.

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

단일 요소의 항목 크기(바이트)입니다. NULL이 아닌 format 값에 호출된 struct.calcsize() 값과 같습니다.

중요한 예외: 소비자가 PyBUF_FORMAT 플래그 없이 버퍼를 요청하면, format은 NULL로 설정되지만, itemsize는 여전히 원래 형식의 값을 갖습니다.

shape이 있으면, product(shape) * itemsize == len 동치가 계속 성립하고 소비자는 itemsize를 사용하여 버퍼를 탐색할 수 있습니다.

PyBUF_SIMPLE 이나 PyBUF_WRITABLE 요청의 결과로 shape이 NULL이면, 소비자는 itemsize를 무시하고 itemsize == 1로 가정해야 합니다.

const char *format

단일 항목의 내용을 설명하는 struct 모듈 스타일 문법의 NUL 종료 문자열. 이것이 NULL이면, "B"(부호 없는 바이트)를 가정합니다.

이 필드는 PyBUF_FORMAT 플래그로 제어됩니다.

int ndim

메모리가 n 차원 배열로 나타내는 차원 수. 0이면, buf는 스칼라를 나타내는 단일 항목을 가리 킵니다. 이 경우, shape, strides 및 suboffsets는 반드시 NULL 이어야 합니다.

매크로 PyBUF_MAX_NDIM는 최대 차원 수를 64로 제한합니다. 제공자는 이 제한을 존중해야 하며, 다차원 버퍼의 소비자는 PyBUF_MAX_NDIM 차원까지 처리할 수 있어야 합니다.

Py_ssize_t *shape

n-차원 배열로 메모리의 모양을 나타내는 길이 ndim의 Py_ssize_t 배열. shape[0] * ... * shape[ndim-1] * itemsize는 len과 같아야 합니다.

모양 값은 shape[n] >= 0로 제한됩니다. shape[n] == 0인 경우는 특별한 주의가 필요합니다. 자세한 정보는 복잡한 배열을 참조하십시오.

shape 배열은 소비자에게 읽기 전용입니다.

Py_ssize_t *strides

각 차원에서 새 요소를 가져오기 위해 건너뛸 바이트 수를 제공하는 길이 ndim의 Py_ssize_t 배열.

스트라이드 값은 임의의 정수일 수 있습니다. 일반 배열의 경우, 스트라이드는 보통 양수이지만, 소비자는 strides[n] <= 0인 경우를 처리할 수 있어야 합니다. 자세한 내용은 복잡한 배열을 참조하십시오.

strides 배열은 소비자에게 읽기 전용입니다.

Py_ssize_t *suboffsets

길이 ndim의 Py_ssize_t 배열. suboffsets[n] >= 0 면, n 번째 차원을 따라 저장된 값은 포인터이고 서브 오프셋 값은 역참조(de-referencing) 후 각 포인터에 더할 바이트 수를 나타냅니다. 음의 서브 오프셋 값은 역참조(de-referencing)가 발생하지 않아야 함을 나타냅니다 (연속 메모리 블록에서의 스트라이드).

모든 서브 오프셋이 음수면 (즉, 역참조가 필요하지 않으면), 이 필드는 NULL(기본값) 이어야 합니다.

이 유형의 배열 표현은 파이썬 이미징 라이브러리(PIL)에서 사용됩니다. 이러한 배열 요소에 액세스하는 방법에 대한 자세한 내용은 복잡한 배열을 참조하십시오.

suboffsets 배열은 소비자에게 읽기 전용입니다.

void *internal

이것은 제공하는(exporting) 객체에 의해 내부적으로 사용됩니다. 예를 들어, 이것은 제공자(exporter)가 정수로 다시 캐스팅할 수 있으며, 버퍼가 해제될 때 shape, strides 및 suboffsets 배열을 해제해야 하는지에 대한 플래그를 저장하는 데 사용됩니다. 소비자가 이 값을 변경해서는 안 됩니다.

버퍼 요청 유형

버퍼는 대개 PyObject_GetBuffer()를 통해 제공하는(exporting) 객체로 버퍼 요청을 보내서 얻습니다. 메모리의 논리적 구조의 복잡성이 크게 다를 수 있으므로, 소비자는 처리할 수 있는 정확한 버퍼 유형을 지정하기 위해 flags 인자를 사용합니다.

모든 Py_buffer 필드는 요청 유형에 의해 모호하지 않게 정의됩니다.

요청 독립적 필드

다음 필드는 flags의 영향을 받지 않고 항상 올바른 값으로 채워져야 합니다: obj, buf, len, itemsize, ndim.

readonly, format

PyBUF_WRITABLE

readonly 필드를 제어합니다. 설정되면, 제공자는 반드시 쓰기 가능한 버퍼를 제공하거나 실패를 보고해야 합니다. 그렇지 않으면, 제공자는 읽기 전용 버퍼나 쓰기 가능 버퍼를 제공할 수 있지만, 모든 소비자에 대해 일관성을 유지해야 합니다.

PyBUF_FORMAT

format 필드를 제어합니다. 설정되면, 이 필드를 올바르게 채워야 합니다. 그렇지 않으면, 이 필드는 반드시 NULL 이어야 합니다.

PyBUF_WRITABLE은 다음 섹션의 모든 플래그와 | 될 수 있습니다. PyBUF_SIMPLE이 0으로 정의되므로, PyBUF_WRITABLE은 독립형 플래그로 사용되어 간단한 쓰기 가능한 버퍼를 요청할 수 있습니다.

PyBUF_FORMAT은 PyBUF_SIMPLE을 제외한 임의의 플래그와 | 될 수 있습니다. PyBUF_SIMPLE은 이미 형식 B(부호 없는 바이트)를 의미합니다.

shape, strides, suboffsets

메모리의 논리 구조를 제어하는 플래그는 복잡도가 감소하는 순서로 나열됩니다. 각 플래그는 그 아래에 있는 플래그의 모든 비트를 포함합니다.

요청

shape

strides

suboffsets

PyBUF_INDIRECT

yes

yes

필요하면

PyBUF_STRIDES

yes

yes

NULL

PyBUF_ND

yes

NULL

NULL

PyBUF_SIMPLE

NULL

NULL

NULL

연속성 요청

C 나 포트란 연속성을 명시적으로 요청할 수 있는데, 스트라이드 정보를 포함하기도 그렇지 않기도 합니다. 스트라이드 정보가 없으면, 버퍼는 C-연속이어야 합니다.

요청

shape

strides

suboffsets

연속성

PyBUF_C_CONTIGUOUS

yes

yes

NULL

C

PyBUF_F_CONTIGUOUS

yes

yes

NULL

F

PyBUF_ANY_CONTIGUOUS

yes

yes

NULL

C 또는 F

PyBUF_ND

yes

NULL

NULL

C

복합 요청

모든 가능한 요청은 앞 절의 플래그 조합에 의해 완전히 정의됩니다. 편의상, 버퍼 프로토콜은 자주 사용되는 조합을 단일 플래그로 제공합니다.

다음 표에서 U는 정의되지 않은 연속성을 나타냅니다. 소비자는 연속성을 판단하기 위해 PyBuffer_IsContiguous()를 호출해야 합니다.

요청

shape

strides

suboffsets

연속성

readonly

format

PyBUF_FULL

yes

yes

필요하면

U

0

yes

PyBUF_FULL_RO

yes

yes

필요하면

U

1 또는 0

yes

PyBUF_RECORDS

yes

yes

NULL

U

0

yes

PyBUF_RECORDS_RO

yes

yes

NULL

U

1 또는 0

yes

PyBUF_STRIDED

yes

yes

NULL

U

0

NULL

PyBUF_STRIDED_RO

yes

yes

NULL

U

1 또는 0

NULL

PyBUF_CONTIG

yes

NULL

NULL

C

0

NULL

PyBUF_CONTIG_RO

yes

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는 실제 메모리 블록 내의 모든 위치를 가리킬 수 있습니다. 제공자(exporter)는 이 함수로 버퍼의 유효성을 검사 할 수 있습니다:

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 스타일 배열에는 차원의 다음 요소를 가져오기 위해 따라야 하는 포인터가 포함될 수 있습니다. 예를 들어, 일반 3-차원 C 배열 char v[2][2][3]는 2개의 2-차원 배열을 가리키는 2개의 포인터 배열로 볼 수도 있습니다: char (*v[2])[2][3]. suboffsets 표현에서, 이 두 포인터는 buf의 시작 부분에 임베드 될 수 있는데, 메모리의 어느 위치 에나 배치될 수 있는 두 개의 char x[2][3] 배열을 가리킵니다.

다음은 NULL이 아닌 strides와 suboffsets가 있을 때, N-차원 인덱스가 가리키는 N-차원 배열의 요소에 대한 포인터를 반환하는 함수입니다:

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