Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 3118 – 버퍼 프로토콜 개정

Author:
Travis Oliphant <oliphant at ee.byu.edu>, Carl Banks <pythondev at aerojockey.com>
Status:
Final
Type:
Standards Track
Created:
28-Aug-2006
Python-Version:
3.0
Post-History:
09-Apr-2007

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at Buffer Protocol, PyBufferProcs, PyMemoryView_FromObject.

×

여기서 제안된 모든 기능이 구현된 것은 아닙니다. 구체적으로는 다음과 같습니다.

이 PEP는 출시된 지 10년이 넘은 Python 3.0을 대상으로 합니다. 누락된 기능을 추가하자는 제안은 이 PEP의 구현을 마무리하는 것으로 취급하지 말고 새로운 기능으로 논의해야 합니다.

See PEP 1 for how to propose changes.

초록

이 PEP는 Python 3.0에서 Python이 메모리 공유를 허용하는 방식을 개선하기 위해 버퍼 인터페이스(PyBufferProcs 함수 포인터)를 재설계할 것을 제안합니다.

특히 API의 문자 버퍼 부분을 제거하고, 스트라이드 메모리의 공유를 허용하는 것과 함께 다중 세그먼트 부분을 재설계할 것을 제안합니다. 또한 새로운 버퍼 인터페이스는 메모리의 모든 다차원 특성과 메모리에 포함된 데이터 형식의 공유를 허용합니다.

이 인터페이스를 사용하면 모든 확장 모듈이 메모리를 공유하는 객체를 생성하거나, 인터페이스를 내보내는 임의의 객체에서 원시 메모리를 사용하고 조작하는 알고리즘을 생성할 수 있습니다.

근거

Python 2.X 버퍼 프로토콜을 사용하면 서로 다른 Python 타입이 내부 버퍼 시퀀스에 대한 포인터를 교환할 수 있습니다. 이 기능은 서로 다른 고수준 객체 간에 대규모 메모리 세그먼트를 공유하는 데 매우 유용하지만, 너무 제한적이며 다음과 같은 문제가 있습니다.

  1. 거의 사용되지 않으며 충분한 근거가 없는 “sequence-of-segments” 옵션(bf_getsegcount)이 있습니다.
  2. 겉보기에 중복되는 문자 버퍼 옵션(bf_getcharbuffer)이 있습니다.
  3. 버퍼 API를 내보내는 객체에 소비자가 메모리 뷰 사용을 “완료”했다고 알릴 방법이 없으므로, 내보내는 객체가 자신이 소유한 메모리의 포인터를 재할당해도 안전한지 확인할 방법이 없습니다(예를 들어, 원래 포인터를 보유한 버퍼 객체와 메모리를 공유한 후 배열 객체가 메모리를 재할당하면 악명 높은 버퍼 객체 문제가 발생했습니다).
  4. 메모리는 길이가 있는 포인터일 뿐입니다. 메모리에 무엇이 들어 있는지(float, int, C 구조체 등)를 설명할 방법이 없습니다.
  5. 메모리에 대한 셰이프 정보가 제공되지 않습니다. 그러나 여러 배열과 유사한 Python 타입은 메모리의 셰이프 해석을 설명하는 표준 방식을 활용할 수 있습니다(wxPython, GTK, pyQT, CVXOPT, PyVox, 오디오 및 비디오 라이브러리, ctypes, NumPy, 데이터베이스 인터페이스 등).
  6. 세그먼트 시퀀스 개념을 사용하는 경우를 제외하면 불연속 메모리를 공유할 방법이 없습니다.

    불연속 메모리 개념을 사용하는 널리 사용되는 라이브러리로는 PIL과 NumPy가 있습니다. 그러나 불연속 배열에 대한 이들의 관점은 서로 다릅니다. 제안된 버퍼 인터페이스는 두 메모리 모델 중 어느 것이든 공유할 수 있도록 합니다. 내보내는 객체는 일반적으로 한 가지 접근 방식만 사용하며, 소비자는 원하는 방식에 따라 각 타입의 불연속 배열을 지원하도록 선택할 수 있습니다.

    NumPy는 각 차원에서 일정한 스트라이딩이라는 개념을 배열의 기본 개념으로 사용합니다. 이 개념을 사용하면 더 큰 배열의 단순한 하위 영역을 데이터를 복사하지 않고 설명할 수 있습니다. 따라서 공유해야 하는 추가 정보는 스트라이드 정보입니다.

    PIL은 더 불투명한 메모리 표현을 사용합니다. 이미지가 메모리의 연속된 세그먼트에 포함되는 경우도 있지만, 이미지의 연속된 세그먼트(대개 행)를 가리키는 포인터 배열에 포함되는 경우도 있습니다. 원래 버퍼 인터페이스에서 여러 버퍼 세그먼트를 사용한다는 아이디어는 PIL에서 비롯되었습니다.

    NumPy의 스트라이드 메모리 모델은 계산 라이브러리에서 더 자주 사용되며, 매우 단순하므로 이 모델을 사용하여 메모리를 공유하도록 지원하는 것이 타당합니다. PIL 메모리 모델은 C 코드에서 때때로 사용되며, 이 경우 2차원 배열에 이중 포인터 간접 참조를 사용하여 접근할 수 있습니다. 예: image[i][j].

    버퍼 인터페이스는 객체가 이러한 메모리 모델 중 어느 것이든 내보낼 수 있도록 허용해야 합니다. 소비자는 연속된 메모리를 요구하거나, 이러한 메모리 모델 중 하나 또는 둘 모두를 처리하는 코드를 작성할 수 있습니다.

제안 개요

  • 버퍼 프로토콜에서 문자 버퍼 및 다중 세그먼트 섹션을 제거하십시오.
  • 버퍼를 가져오는 읽기 및 쓰기 버전을 통합하십시오.
  • 소비자 객체가 메모리 영역 사용을 “완료”했을 때 호출해야 하는 새 함수를 인터페이스에 추가하십시오.
  • 메모리에 어떤 내용이 있는지 인터페이스가 설명할 수 있도록 새 변수를 추가하십시오(struct와 array에서 현재 수행되는 작업을 통합합니다).
  • 프로토콜이 셰이프 정보를 공유할 수 있도록 새 변수를 추가하십시오.
  • 스트라이드 정보를 공유하기 위한 새 변수를 추가하십시오.
  • 포인터 간접 참조를 사용하여 접근해야 하는 배열을 공유하기 위한 새 메커니즘을 추가하십시오.
  • 코어와 표준 라이브러리의 모든 객체를 새 인터페이스에 맞도록 수정하십시오.
  • 더 많은 형식 지정자를 처리하도록 struct 모듈을 확장하십시오.
  • 버퍼 인터페이스를 감싸는 Python 외관을 제공하는 새 메모리 객체로 버퍼 객체를 확장하십시오.
  • 버퍼 인터페이스를 지원하는 객체 안팎으로 연속된 데이터를 쉽게 복사할 수 있는 몇 가지 함수를 추가하십시오.

사양

새 사양은 복잡한 메모리 공유를 허용하지만, 객체에서 간단한 연속 바이트 버퍼를 여전히 얻을 수 있습니다. 실제로 새 프로토콜은 원래 객체가 연속된 메모리 덩어리로 표현되지 않는 경우에도 이를 수행하기 위한 표준 메커니즘을 허용합니다.

간단한 연속 메모리 덩어리를 얻는 가장 쉬운 방법은 제공된 C-API를 사용하여 메모리 덩어리를 얻는 것입니다.

PyBufferProcs 구조체를 다음과 같이 변경하십시오.

typedef struct {
     getbufferproc bf_getbuffer;
     releasebufferproc bf_releasebuffer;
} PyBufferProcs;

이 두 루틴은 타입 객체에서 선택 사항입니다.

typedef int (*getbufferproc)(PyObject *obj, PyBuffer *view, int flags)

이 함수는 성공하면 0을 반환하고, 실패하면 오류를 발생시키면서 -1을 반환합니다. 첫 번째 변수는 “내보내는” 객체입니다. 두 번째 인자는 bufferinfo 구조체의 주소입니다. 두 인자 모두 절대로 NULL이어서는 안 됩니다.

세 번째 인자는 소비자가 처리할 준비가 된 버퍼의 종류를 나타내며, 따라서 익스포터가 반환할 수 있는 버퍼의 종류를 나타냅니다. 새 버퍼 인터페이스는 훨씬 더 복잡한 메모리 공유 가능성을 허용합니다. 일부 소비자는 이러한 모든 복잡성을 처리하지 못할 수 있지만, 익스포터가 자신의 메모리를 더 단순한 관점으로 볼 수 있도록 허용하는지 확인하려 할 수 있습니다.

또한 일부 익스포터는 가능한 모든 방식으로 메모리를 공유하지 못할 수 있으며, 일부 소비자에게 무언가가 단순히 불가능하다는 것을 알리기 위해 오류를 발생시켜야 할 수 있습니다. 실제로 문제를 일으키는 다른 오류가 없는 한 이러한 오류는 PyErr_BufferError이어야 합니다. 익스포터는 플래그 정보를 사용하여 PyBuffer 구조체를 비기본값으로 채우는 정도를 간소화하거나, 객체가 메모리에 대한 더 단순한 뷰를 지원할 수 없는 경우 오류를 발생시킬 수 있습니다.

익스포터는 항상 버퍼 구조체의 모든 요소를 채워야 합니다(다른 요청이 없으면 기본값 또는 NULL을 사용합니다). 간단한 경우에는 PyBuffer_FillInfo 함수를 사용할 수 있습니다.

액세스 플래그

일부 플래그는 특정 종류의 메모리 세그먼트를 요청하는 데 유용하며, 다른 플래그는 소비자가 처리할 수 있는 정보의 종류를 익스포터에 나타냅니다. 소비자가 특정 정보를 요청하지 않았지만 익스포터가 해당 정보 없이는 메모리를 공유할 수 없는 경우에는 PyErr_BufferError를 발생시켜야 합니다.

PyBUF_SIMPLE

이는 기본 플래그 상태(0)입니다. 반환되는 버퍼에는 쓰기 가능한 메모리가 있을 수도 있고 없을 수도 있습니다. 형식은 부호 없는 바이트로 간주됩니다. 이는 “독립형” 플래그 상수입니다. 다른 플래그와 |’d할 필요가 전혀 없습니다. 익스포터가 이러한 바이트의 연속 버퍼를 제공할 수 없으면 오류를 발생시킵니다.

PyBUF_WRITABLE

반환되는 버퍼는 쓰기 가능해야 합니다. 쓰기 가능하지 않으면 오류를 발생시켜야 합니다.

PyBUF_FORMAT

이 플래그가 제공되면 반환되는 버퍼에는 실제 형식 정보가 있어야 합니다. 이는 소비자가 실제로 저장된 데이터의 ‘종류’를 확인할 때 사용됩니다. 익스포터는 요청된 경우 항상 이 정보를 제공할 수 있어야 합니다. 형식을 명시적으로 요청하지 않은 경우 형식은 NULL로 반환되어야 합니다(이는 “B”, 즉 부호 없는 바이트를 의미합니다).

PyBUF_ND

반환되는 버퍼는 셰이프 정보를 제공해야 합니다. 메모리는 C 스타일의 연속 메모리로 간주됩니다(마지막 차원이 가장 빠르게 변합니다). 익스포터가 이러한 종류의 연속 버퍼를 제공할 수 없으면 오류를 발생시킬 수 있습니다. 이 플래그가 지정되지 않으면 shape는 NULL입니다.

PyBUF_STRIDES (PyBUF_ND를 암시합니다)

반환된 버퍼는 스트라이드 정보를 제공해야 합니다(즉, strides는 NULL일 수 없습니다). 이는 소비자가 스트라이드가 있고 비연속적인 배열을 처리할 수 있을 때 사용됩니다. 스트라이드를 처리할 수 있다는 것은 자동으로 shape도 처리할 수 있음을 전제로 합니다. 내보내기 객체는 데이터의 스트라이드 전용 표현(즉, suboffsets가 없는 표현)을 제공할 수 없는 경우 오류를 발생시킬 수 있습니다.
PyBUF_C_CONTIGUOUS
PyBUF_F_CONTIGUOUS
PyBUF_ANY_CONTIGUOUS
이 플래그들은 반환된 버퍼가 각각 C 연속적(마지막 차원이 가장 빠르게 변함), Fortran 연속적(첫 번째 차원이 가장 빠르게 변함) 또는 둘 중 하나여야 함을 나타냅니다. 이 모든 플래그는 PyBUF_STRIDES를 의미하며 스트라이드 버퍼 정보 구조체가 올바르게 채워지도록 보장합니다.

PyBUF_INDIRECT (PyBUF_STRIDES를 의미합니다)

반환된 버퍼에는 suboffsets 정보가 있어야 합니다(하위 오프셋이 필요하지 않은 경우에는 NULL일 수 있습니다). 이는 소비자가 이러한 suboffsets로 암시되는 간접 배열 참조를 처리할 수 있을 때 사용됩니다.

특정 종류의 memory_sharing을 위한 플래그의 특수 조합입니다.

다차원(단, 연속적)
PyBUF_CONTIG (PyBUF_ND | PyBUF_WRITABLE)
PyBUF_CONTIG_RO (PyBUF_ND)

스트라이드를 사용하는 다차원 배열이지만 정렬됨

PyBUF_STRIDED (PyBUF_STRIDES | PyBUF_WRITABLE)
PyBUF_STRIDED_RO (PyBUF_STRIDES)

스트라이드를 사용하는 다차원 배열이며 반드시 정렬되어 있지는 않음

PyBUF_RECORDS (PyBUF_STRIDES | PyBUF_WRITABLE | PyBUF_FORMAT)
PyBUF_RECORDS_RO (PyBUF_STRIDES | PyBUF_FORMAT)

sub-offsets를 사용하는 다차원 배열

PyBUF_FULL (PyBUF_INDIRECT | PyBUF_WRITABLE | PyBUF_FORMAT)
PyBUF_FULL_RO (PyBUF_INDIRECT | PyBUF_FORMAT)

따라서 객체에서 단순히 연속된 바이트 덩어리를 얻으려는 소비자는 PyBUF_SIMPLE을 사용하는 반면, 가장 복잡한 경우를 활용하는 방법을 이해하는 소비자는 PyBUF_FULL을 사용할 수 있습니다.

형식 정보는 플래그 인자에 PyBUF_FORMAT이 포함된 경우에만 NULL이 아님이 보장되며, 그렇지 않으면 소비자가 부호 없는 바이트라고 간주할 것으로 예상됩니다.

내보낼 수 있는 것이 연속된 “부호 없는 바이트” 덩어리뿐인 경우, 단순한 내보내기 객체가 제공된 플래그에 따라 버퍼 정보 구조체를 올바르게 채우는 데 사용할 수 있는 C-API가 있습니다.

Py_buffer 구조체

bufferinfo 구조체는 다음과 같습니다.:

struct bufferinfo {
     void *buf;
     Py_ssize_t len;
     int readonly;
     const char *format;
     int ndim;
     Py_ssize_t *shape;
     Py_ssize_t *strides;
     Py_ssize_t *suboffsets;
     Py_ssize_t itemsize;
     void *internal;
} Py_buffer;

bf_getbuffer 함수를 호출하기 전에는 bufferinfo 구조체를 무엇으로든 채워 둘 수 있지만, 새 버퍼를 요청할 때는 buf필드가 NULL이어야 합니다. bf_getbuffer에서 반환되면 bufferinfo 구조체에는 버퍼와 관련된 정보가 채워집니다. 소비자가 메모리 사용을 마치면 동일한 bufferinfo 구조체를 bf_releasebuffer에 전달해야 합니다(사용 가능한 경우). 호출자는 releasebuffer가 호출될 때까지 obj에 대한 참조를 유지할 책임이 있습니다(즉, bf_getbuffer 호출은 obj의 참조 횟수를 변경하지 않습니다).

bufferinfo 구조체의 멤버는 다음과 같습니다.

buf
객체 메모리의 시작 부분을 가리키는 포인터
len
객체가 사용하는 전체 메모리 바이트 수입니다. 이는 shape 배열의 곱에 메모리 항목당 바이트 수를 곱한 값과 같아야 합니다.
readonly
메모리가 읽기 전용인지 여부를 저장하는 정수 변수입니다. 1은 메모리가 읽기 전용임을 의미하고, 0은 메모리에 쓸 수 있음을 의미합니다.
format
메모리의 각 요소에 무엇이 들어 있는지를 나타내는 NULL로 끝나는 형식 문자열입니다(확장을 포함한 구조체 스타일 구문을 따릅니다). 요소의 개수는 len / itemsize이며, 여기서 itemsize는 형식이 의미하는 바이트 수입니다. 이는 NULL일 수 있으며, 이 경우 표준 부호 없는 바이트(“B”)를 의미합니다.
ndim
메모리가 나타내는 차원 수를 저장하는 변수입니다. 0 이상이어야 합니다. 값이 0이면 shape, strides 및 suboffsets가 NULL이어야 합니다(즉, 메모리가 스칼라를 나타냅니다).
shape
메모리를 N차원 배열로 나타낼 때 그 형태를 지정하는, 길이가 ndimsPy_ssize_t 배열입니다. ((*shape)[0] * ... * (*shape)[ndims-1])*itemsize = len임에 유의하십시오. ndims가 0인 경우(스칼라를 나타냄) 이는 NULL이어야 합니다.
strides
길이가 ndimsPy_ssize_t 배열을 가리키는 포인터(또는 ndims가 0이면 NULL)로 채워질 Py_ssize_t* 변수의 주소이며, 각 차원에서 다음 요소로 이동하기 위해 건너뛰어야 하는 바이트 수를 나타냅니다. 호출자가 이를 요청하지 않은 경우(PyBUF_STRIDES가 설정되지 않은 경우) C 스타일의 연속 배열을 나타내도록 NULL로 설정하거나, 이것이 불가능하면 PyExc_BufferError를 발생시켜야 합니다.
suboffsets
길이가 *ndimsPy_ssize_t 배열을 가리키는 포인터로 채워질 Py_ssize_t * 변수의 주소입니다. 이러한 suboffset 값이 >=0이면, 해당 차원을 따라 저장된 값은 포인터이며 suboffset 값은 역참조한 후 포인터에 추가할 바이트 수를 지정합니다. suboffset 값이 음수이면 역참조를 수행하지 않아야 함을 나타냅니다(연속 메모리 블록에서의 스트라이딩). 모든 suboffset가 음수이면(즉, 역참조가 필요하지 않으면) 이는 NULL이어야 합니다(기본값). 호출자가 이를 요청하지 않은 경우(PyBUF_INDIRECT가 설정되지 않은 경우) NULL로 설정하거나, 이것이 불가능하면 PyExc_BufferError를 발생시켜야 합니다.

명확히 하기 위해, 비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;
}

역참조가 수행된 “후에” suboffset가 추가된다는 점에 유의하십시오. 따라서 i번째 차원에서 슬라이싱하면 (i-1)번째 차원의 suboffsets에 값이 추가됩니다. 첫 번째 차원에서 슬라이싱하면 시작 포인터의 위치가 직접 변경됩니다(즉, buf가 수정됩니다).

itemsize
이는 공유 메모리의 각 요소에 대한 itemsize(바이트 단위)를 저장합니다. 이는 PyBuffer_SizeFromFormat을 사용하여 얻을 수 있으므로 기술적으로 필요하지 않지만, 내보내기 객체는 형식 문자열을 구문 분석하지 않고도 이 정보를 알고 있을 수 있으며 스트라이딩을 올바르게 해석하려면 itemsize를 알아야 합니다. 따라서 이를 저장하는 편이 더 편리하고 빠릅니다.
internal
이는 내보내기 객체가 내부적으로 사용하기 위한 것입니다. 예를 들어 내보내기 객체가 이를 정수로 다시 형변환하여 버퍼가 해제될 때 shape, strides 및 suboffsets 배열을 해제해야 하는지 여부에 관한 플래그를 저장하는 데 사용할 수 있습니다. 소비자는 이 값을 절대로 변경해서는 안 됩니다.

익스포터는 buf, format, shape, strides 및 suboffsets가 가리키는 모든 메모리가 releasebuffer가 호출될 때까지 유효하도록 보장할 책임이 있습니다. 익스포터가 releasebuffer가 호출되기 전에 객체의 shape, strides 및/또는 suboffsets를 변경할 수 있어야 한다면, getbuffer가 호출될 때 해당 배열을 할당하고(제공된 버퍼 정보 구조체에서 해당 배열을 가리키도록 하여) releasebuffer가 호출될 때 해제해야 합니다.

버퍼 해제

release-buffer 인터페이스 호출에서는 동일한 bufferinfo 구조체를 사용해야 합니다. 호출자는 Py_buffer 구조체 자체의 메모리를 책임집니다.

typedef void (*releasebufferproc)(PyObject *obj, Py_buffer *view)

getbufferproc 호출자는 객체에서 이전에 획득한 메모리가 더 이상 필요하지 않을 때 이 함수가 호출되도록 해야 합니다. 인터페이스의 익스포터는 bufferinfo 구조체에서 가리키는 모든 메모리가 releasebuffer가 호출될 때까지 유효하게 유지되도록 해야 합니다.

bf_releasebuffer 함수가 제공되지 않는 경우(즉, NULL인 경우)에는 이를 호출할 필요가 전혀 없습니다.

익스포터가 struct bufferinfo를 통해 공유할 수 있는 메모리, strides, shape, suboffsets 또는 format 변수를 재할당할 수 있다면 bf_releasebuffer 함수를 정의해야 합니다. 수행되고 공유된 getbuffer 호출 횟수를 추적하기 위해 여러 메커니즘을 사용할 수 있습니다. 익스포트된 “뷰”의 수를 추적하는 단일 변수를 사용하거나, 각 객체에서 작성된 bufferinfo 구조체의 연결 리스트를 유지 관리할 수 있습니다.

그러나 익스포터에 구체적으로 요구되는 것은 bufferinfo 구조체를 통해 공유된 모든 메모리가 해당 메모리를 익스포트하는 bufferinfo 구조체에서 releasebuffer가 호출될 때까지 유효하게 유지되도록 보장하는 것뿐입니다.

새로운 C-API 호출이 제안됩니다.

int PyObject_CheckBuffer(PyObject *obj)

getbuffer 함수가 사용 가능하면 1을, 그렇지 않으면 0을 반환합니다.

int PyObject_GetBuffer(PyObject *obj, Py_buffer *view,
                       int flags)

이는 getbuffer 함수 호출의 C-API 버전입니다. 객체에 필요한 함수 포인터가 있는지 확인한 후 호출을 실행합니다. 실패하면 -1을 반환하고 오류를 발생시키며, 성공하면 0을 반환합니다.

void PyBuffer_Release(PyObject *obj, Py_buffer *view)

이는 releasebuffer 함수 호출의 C-API 버전입니다. 객체에 필요한 함수 포인터가 있는지 확인한 후 호출을 실행합니다. 객체에 releasebuffer 함수가 없는 경우에도 이 함수는 항상 성공합니다.

PyObject *PyObject_GetMemoryView(PyObject *obj)

버퍼 인터페이스를 정의하는 객체에서 메모리 뷰 객체를 반환합니다.

메모리 뷰 객체는 버퍼 객체를 대체할 수 있는 확장된 버퍼 객체입니다(단순한 1차원 메모리 뷰 객체로 유지할 수도 있으므로 반드시 대체해야 하는 것은 아닙니다). 해당 C 구조체는 다음과 같습니다.

typedef struct {
    PyObject_HEAD
    PyObject *base;
    Py_buffer view;
} PyMemoryViewObject;

이는 현재 버퍼 객체와 기능적으로 유사하지만 base에 대한 참조가 유지되고 메모리 뷰를 다시 획득하지 않습니다. 따라서 이 메모리 뷰 객체는 삭제될 때까지 base의 메모리를 유지합니다.

이 메모리 뷰 객체는 다차원 슬라이싱을 지원하며, Python에서 이를 지원하는 최초의 객체가 됩니다. 메모리 뷰 객체의 슬라이스는 동일한 base를 가지지만 base 객체에 대한 뷰가 다른 다른 메모리 뷰 객체입니다.

메모리 뷰에서 “요소”가 반환될 때 이는 항상 bytes 객체이며, 해당 형식은 memoryview 객체의 format 속성에 따라 해석해야 합니다. 원하는 경우 Python에서 struct 모듈을 사용하여 바이트를 “디코드”할 수 있습니다. 또는 내용을 NumPy 배열이나 버퍼 프로토콜을 사용하는 다른 객체에 전달할 수 있습니다.

Python 이름은 다음과 같습니다.

__builtin__.memoryview

메서드:

__getitem__ (will support multi-dimensional slicing)
__setitem__ (will support multi-dimensional slicing)
tobytes (obtain a new bytes-object of a copy of the memory).
tolist (obtain a “nested” list of the memory. Everything struct 모듈의 unpack가 수행하는 것처럼 표준 Python 객체로 해석됩니다 – 실제로는 struct.unpack을 사용하여 이를 수행합니다).

속성(베이스 객체의 메모리에서 가져옴):

  • format
  • itemsize
  • shape
  • strides
  • suboffsets
  • readonly
  • ndim
Py_ssize_t PyBuffer_SizeFromFormat(const char *)

struct 스타일 설명에서 데이터 형식 영역의 암시된 itemsize를 반환합니다.

PyObject * PyMemoryView_GetContiguous(PyObject *obj,  int buffertype,
                                      char fortran)

obj로 표현되는 연속 메모리 청크에 대한 memoryview 객체를 반환합니다. 복사본을 만들어야 하는 경우(obj가 가리키는 메모리가 연속적이지 않기 때문), 새 bytes 객체가 생성되어 반환되는 memory view 객체의 베이스 객체가 됩니다.

buffertype 인자는 PyBUF_READ, PyBUF_WRITE, PyBUF_UPDATEIFCOPY 중 하나일 수 있으며, 복사본을 만들어야 하는 경우 반환되는 버퍼를 읽을 수 있는지, 쓸 수 있는지 또는 원래 버퍼를 업데이트하도록 설정할지를 결정합니다. buffertype이 PyBUF_WRITE이고 버퍼가 연속적이지 않으면 오류가 발생합니다. 이 경우 사용자는 PyBUF_UPDATEIFCOPY를 사용하여 쓰기 가능한 임시 연속 버퍼가 반환되도록 할 수 있습니다. 원래 객체에 쓸 수 있는 한, memoryview 객체가 삭제된 후 이 연속 버퍼의 내용이 원래 객체로 다시 복사됩니다. 원래 객체가 이를 허용하지 않으면 BufferError가 발생합니다.

객체가 다차원인 경우 fortran이 ‘F’이면 기본 배열의 첫 번째 차원이 버퍼에서 가장 빠르게 변합니다. fortran이 ‘C’이면 마지막 차원이 가장 빠르게 변합니다(C 스타일의 연속 배열). fortran이 ‘A’이면 어느 쪽이든 상관없으며, 객체가 더 효율적이라고 판단하는 방식을 사용하게 됩니다. 복사본이 생성되면 PyMem_Free를 호출하여 메모리를 해제해야 합니다.

memoryview 객체에 대한 새 참조를 받습니다.

int PyObject_CopyToObject(PyObject *obj, void *buf, Py_ssize_t len,
                          char fortran)

buf가 가리키는 연속 메모리 청크가 가리키는 데이터의 len바이트를 obj가 내보내는 버퍼로 복사합니다. 성공하면 0을 반환하고, 실패하면 -1을 반환하며 오류를 발생시킵니다. 객체에 쓰기 가능한 버퍼가 없으면 오류가 발생합니다. fortran이 ‘F’이고 객체가 다차원인 경우, 데이터가 Fortran 방식으로 배열에 복사됩니다(첫 번째 차원이 가장 빠르게 변합니다). fortran이 ‘C’이면 데이터가 C 방식으로 배열에 복사됩니다(마지막 차원이 가장 빠르게 변합니다). fortran이 ‘A’이면 어느 쪽이든 상관없으며, 더 효율적인 방식으로 복사됩니다.

int PyObject_CopyData(PyObject *dest, PyObject *src)

이 마지막 세 가지 C-API 호출을 사용하면 Python 객체에 실제로 어떻게 저장되어 있는지와 관계없이 연속 메모리 영역에서 데이터를 가져오고 Python 객체로 데이터를 내보내는 표준적인 방법을 사용할 수 있습니다. 이러한 호출은 확장 버퍼 인터페이스를 사용하여 작업을 수행합니다.

int PyBuffer_IsContiguous(Py_buffer *view, char fortran)

뷰 객체가 정의하는 메모리가 C 스타일(fortran = ‘C’) 또는 Fortran 스타일(fortran = ‘F’)로 연속적이거나 둘 중 하나(fortran = ‘A’)이면 1을 반환합니다. 그렇지 않으면 0을 반환합니다.

void PyBuffer_FillContiguousStrides(int ndim, Py_ssize_t *shape,
                                    Py_ssize_t *strides, Py_ssize_t itemsize,
                                    char fortran)

지정된 shape와 요소당 지정된 바이트 수를 사용하여 연속 배열(fortran이 ‘C’이면 C 스타일, fortran이 ‘F’이면 Fortran 스타일)의 바이트 스트라이드로 strides 배열을 채웁니다.

int PyBuffer_FillInfo(Py_buffer *view, void *buf,
                      Py_ssize_t len, int readonly, int infoflags)

지정된 길이의 “unsigned bytes”로 이루어진 연속 메모리 청크만 공유할 수 있는 익스포터에 대해 버퍼 정보 구조체를 올바르게 채웁니다. 성공하면 0을 반환하고, 오류가 발생하면 오류를 발생시키면서 -1을 반환합니다.

PyExc_BufferError

익스포터가 소비자가 예상하는 종류의 버퍼를 제공할 수 없어서 발생하는 버퍼 오류를 반환하기 위한 새 오류 객체입니다. 소비자가 프로토콜을 제공하지 않는 객체에서 버퍼를 요청하는 경우에도 이 오류가 발생합니다.

struct 문자열 구문에 대한 추가 사항

struct 문자열 구문에는 다른 곳(예를 들어 ctypes와 NumPy)에서 이미 제공되는 데이터 형식 설명을 완전히 구현하는 데 필요한 일부 문자가 빠져 있습니다. Python 2.5 명세는 http://docs.python.org/library/struct.html 에 있습니다.

다음은 제안된 추가 사항입니다.

Character Description
‘t’ bit (number before states how many bits)
‘?’ platform _Bool type
‘g’ long double
‘c’ ucs-1 (latin-1) encoding
‘u’ ucs-2
‘w’ ucs-4
‘O’ pointer to Python Object
‘Z’ complex (whatever the next specifier is)
‘&’ specific pointer (prefix before another character)
‘T{}’ structure (detailed layout inside {})
‘(k1,k2,…,kn)’ multi-dimensional array of whatever follows
‘:name:’ optional name of the preceding element
‘X{}’
pointer to a function (optional function
signature inside {} with any return value preceded by -> and placed at the end)

struct 모듈이 이러한 항목도 이해하고 언패킹할 때 적절한 Python 객체를 반환하도록 변경합니다. long-double을 언패킹하면 decimal 객체 또는 ctypes long-double을 반환합니다. ‘u’ 또는 ‘w’를 언패킹하면 Python 유니코드를 반환합니다. 다차원 배열을 언패킹하면 리스트를 반환합니다(1차원을 초과하면 리스트의 리스트). 포인터를 언패킹하면 ctypes 포인터 객체를 반환합니다. 함수 포인터를 언패킹하면 ctypes 호출 객체를 반환합니다(아마도). 비트를 언패킹하면 Python Bool을 반환합니다. struct 문자열 구문의 공백은 아직 무시되지 않는 경우 무시합니다. 명명된 객체를 언패킹하면 튜플처럼 동작하면서 해당 항목에 이름으로도 접근할 수 있는 일종의 명명된 튜플과 유사한 객체를 반환합니다. 중첩 구조체를 언패킹하면 중첩 튜플을 반환합니다.

엔디언 지정(‘!’, ‘@’,’=’,’>’,’<’, ‘^’)은 필요할 때 변경할 수 있도록 문자열 내부에서도 허용됩니다. 이전에 지정된 엔디언 문자열은 변경될 때까지 적용됩니다. 기본 엔디언은 ‘@’이며, 이는 네이티브 데이터 형식과 정렬을 의미합니다. 정렬되지 않은 네이티브 데이터 형식을 요청하는 경우 엔디언 지정은 ‘^’입니다.

struct 모듈에 따르면 문자 코드 앞에 숫자를 사용하여 해당 형식의 개수를 지정할 수 있습니다. (k1,k2,...,kn) 확장을 사용하면 데이터가 특정 형식의 다차원 배열(C 스타일의 연속 배열이며 마지막 차원이 가장 빠르게 변함)로 간주되어야 하는지도 지정할 수 있습니다.

구조체 설명에서 ctypes 객체를 생성하는 함수와 long-double 및 ucs-2를 ctypes에 추가하는 함수가 추가되어야 합니다.

데이터 형식 설명의 예

다음은 C 구조체와 이를 struct 스타일 구문으로 표현하는 방법의 몇 가지 예입니다.

<named>는 명명된 튜플의 생성자입니다(아직 지정되지 않음).

float
'd' <–> Python float
복소 double
'Zd' <–> Python complex
RGB 픽셀 데이터
'BBB' <–> (int, int, int) 'B:r: B:g: B:b:' <–> <named>((int, int, int), (‘r’,’g’,’b’))
혼합 엔디언(이상하지만 가능합니다)
'>i:big: <i:little:' <–> <named>((int, int), (‘big’, ‘little’))
중첩 구조
struct {
     int ival;
     struct {
         unsigned short sval;
         unsigned char bval;
         unsigned char cval;
     } sub;
}
"""i:ival:
   T{
      H:sval:
      B:bval:
      B:cval:
    }:sub:
"""
중첩 배열
struct {
     int ival;
     double data[16*4];
}
"""i:ival:
   (16,4)d:data:
"""

마지막 예제에서 비교 대상인 C 구조체는 2차원 배열인 data[16][4]가 아니라 의도적으로 1차원 배열이라는 점에 유의하십시오. 이는 C에서 정적으로 할당된 다차원 배열(연속적으로 배치됨)과 요소에 접근할 때 동일한 구문인 data[0][1]을 사용하지만 메모리가 반드시 연속적이지는 않은 동적 다차원 배열 사이에서 발생하는 혼동을 피하기 위한 것입니다. struct 구문은 항상 연속 메모리를 사용하며, 다차원 특성은 익스포터가 전달할 메모리에 관한 정보입니다.

다시 말해, 동일한 메모리 레이아웃을 설명하는 한 struct 구문 설명이 C 구문과 정확히 일치할 필요는 없습니다. C 컴파일러가 메모리를 double의 1차원 배열로 간주한다는 사실은, 익스포터가 소비자에게 이 메모리 필드를 4개 요소마다 새 차원이 추가되는 2차원 배열로 간주해야 한다고 전달하려 했다는 사실과는 무관합니다.

영향을 받는 코드

이전 버퍼 인터페이스를 익스포트하거나 소비하는 Python의 모든 객체와 모듈이 수정됩니다. 다음은 일부 목록입니다.

  • buffer 객체
  • bytes 객체
  • string 객체
  • unicode 객체
  • array 모듈
  • struct 모듈
  • mmap 모듈
  • ctypes 모듈

버퍼 API를 사용하는 그 밖의 모든 것

이슈 및 세부 사항

기존 버퍼 프로토콜에 C-API와 두 함수를 추가하여 이 PEP를 Python 2.6으로 백포트할 예정입니다.

이 PEP의 이전 버전에서는 읽기/쓰기 잠금 방식을 제안했지만, 이후 a) 잠금이 필요하지 않은 일반적인 단순 사용 사례에는 너무 복잡하고 b) 변경되는 짧은 수명의 잠금으로 버퍼에 동시에 읽기/쓰기 접근해야 하는 사용 사례에는 너무 단순하다는 인식이 생겼습니다. 따라서 동시 읽기/쓰기 접근 전반에서 일관된 뷰가 필요한 경우 사용자가 버퍼 객체 주변에 자신만의 특정 잠금 방식을 구현하도록 맡깁니다. 이러한 사용자 방식에 대한 경험을 어느 정도 축적한 후 별도의 잠금 API를 포함하는 향후 PEP가 제안될 수 있습니다.

스트라이드 메모리와 서브오프셋을 공유하는 것은 새로운 기능이며, 다중 세그먼트 인터페이스를 수정한 것으로 볼 수 있습니다. 이는 NumPy와 PIL에서 동기를 얻었습니다. 계산 라이브러리와 인터페이스할 때 스트라이드 메모리가 매우 흔하기 때문에, NumPy 객체는 스트라이드 메모리를 관리하는 방법을 이해하는 코드와 자신의 스트라이드 메모리를 공유할 수 있어야 합니다.

또한 이 접근 방식을 사용하면 복사하지 않고도 두 종류의 메모리 모두에서 작동하는 제네릭 코드를 작성할 수 있어야 합니다.

bufferinfo 구조체의 format 문자열, shape 배열, strides 배열 및 suboffsets 배열에 대한 메모리 관리는 항상 내보내는 객체의 책임입니다. 소비자는 이러한 포인터를 다른 메모리로 설정하거나 해제하려고 해서는 안 됩니다.

여러 아이디어가 논의되었으나 거부되었습니다.

release-buffer가 호출되는 “releaser” 객체를 두는 방안입니다. 이 방안은 프로토콜을 비대칭적으로 만들었기 때문에 (버퍼를 “얻은” 대상과 다른 대상에 대해 release를 호출하게 되므로) 받아들일 수 없는 것으로 판단되었습니다. 또한 실질적인 이점은 제공하지 않으면서 프로토콜을 복잡하게 만들었습니다.

모든 구조체 변수를 함수에 개별적으로 전달하는 방안입니다. 이 방안은 관심 대상이 아닌 변수에 NULL을 설정할 수 있다는 장점이 있었지만, 함수 호출을 더 어렵게 만들기도 했습니다. flags 변수는 소비자가 프로토콜을 호출하는 방식을 “단순하게” 유지하면서 동일한 기능을 사용할 수 있도록 합니다.

코드입니다.

PEP의 작성자들은 이 제안의 코드를 제공하고 유지 관리할 것을 약속하지만, 어떠한 도움도 환영합니다.

예제입니다.

예제 1입니다.

이 예제에서는 연속적인 라인을 사용하는 이미지 객체가 자신의 버퍼를 노출하는 방식을 보여 줍니다.:

struct rgba {
    unsigned char r, g, b, a;
};

struct ImageObject {
    PyObject_HEAD;
    ...
    struct rgba** lines;
    Py_ssize_t height;
    Py_ssize_t width;
    Py_ssize_t shape_array[2];
    Py_ssize_t stride_array[2];
    Py_ssize_t view_count;
};

“lines”는 malloc으로 할당된 1차원 배열 (struct rgba*)를 가리킵니다. 해당 블록의 각 포인터는 별도로 malloc으로 할당된 (struct rgba) 배열을 가리킵니다.

예를 들어 x=30, y=50에 있는 픽셀의 빨간색 값에 액세스하려면 “lines[50][30].r”을 사용합니다.

그렇다면 ImageObject의 getbuffer는 무엇을 수행할까요? 오류 검사는 생략합니다.:

int Image_getbuffer(PyObject *self, Py_buffer *view, int flags) {

    static Py_ssize_t suboffsets[2] = { 0, -1};

    view->buf = self->lines;
    view->len = self->height*self->width;
    view->readonly = 0;
    view->ndims = 2;
    self->shape_array[0] = height;
    self->shape_array[1] = width;
    view->shape = &self->shape_array;
    self->stride_array[0] = sizeof(struct rgba*);
    self->stride_array[1] = sizeof(struct rgba);
    view->strides = &self->stride_array;
    view->suboffsets = suboffsets;

    self->view_count ++;

    return 0;
}


int Image_releasebuffer(PyObject *self, Py_buffer *view) {
    self->view_count--;
    return 0;
}

예제 2입니다.

이 예제에서는 객체가 살아 있는 동안 재할당되지 않는 연속적인 메모리 덩어리를 노출하려는 객체가 이를 수행하는 방식을 보여 줍니다.

int myobject_getbuffer(PyObject *self, Py_buffer *view, int flags) {

    void *buf;
    Py_ssize_t len;
    int readonly=0;

    buf = /* Point to buffer */
    len = /* Set to size of buffer */
    readonly = /* Set to 1 if readonly */

    return PyObject_FillBufferInfo(view, buf, len, readonly, flags);
}

/* No releasebuffer is necessary because the memory will never
   be re-allocated
*/

예제 3입니다.

Python 객체 obj에서 단순한 연속 바이트 덩어리만 가져오려는 소비자는 다음과 같이 합니다.

Py_buffer view;
int ret;

if (PyObject_GetBuffer(obj, &view, Py_BUF_SIMPLE) < 0) {
     /* error return */
}

/* Now, view.buf is the pointer to memory
        view.len is the length
        view.readonly is whether or not the memory is read-only.
 */


/* After using the information and you don't need it anymore */

if (PyBuffer_Release(obj, &view) < 0) {
        /* error return */
}

예제 4입니다.

어떤 객체의 메모리든 사용할 수 있지만 연속 메모리만 처리하는 알고리즘을 작성하는 소비자는 다음과 같이 할 수 있습니다.

void *buf;
Py_ssize_t len;
char *format;
int copy;

copy = PyObject_GetContiguous(obj, &buf, &len, &format, 0, 'A');
if (copy < 0) {
   /* error return */
}

/* process memory pointed to by buffer if format is correct */

/* Optional:

   if, after processing, we want to copy data from buffer back
   into the object

   we could do
   */

if (PyObject_CopyToObject(obj, buf, len, 'A') < 0) {
       /*        error return */
}

/* Make sure that if a copy was made, the memory is freed */
if (copy == 1) PyMem_Free(buf);