[Final] PEP 3118 - Revising the buffer protocol
원문 링크: PEP 3118 - Revising the buffer protocol
상태: Final 유형: Standards Track 작성일: 28-Aug-2006
PEP 3118 – 버퍼 프로토콜 재설계 (Revising the buffer protocol)
- 작성자: Travis Oliphant, Carl Banks
- 상태: Final
- 유형: Standards Track
- 생성일: 2006년 8월 28일
- Python 버전: 3.0
- 최종 수정일: 2025년 2월 14일
중요: 이 PEP는 역사적인 문서입니다. 최신 공식 문서는 Buffer Protocol, PyBufferProcs, PyMemoryView_FromObject에서 찾을 수 있습니다.
이 PEP에서 제안된 모든 기능이 구현된 것은 아닙니다. 특히:
PyObject_CopyToObject
는 추가되지 않았습니다.struct
문자열 구문 추가 사항 중?
(_Bool
)를 제외한 나머지는 추가되지 않았습니다.PyObject_GetMemoryView
는PyMemoryView_FromObject
로 이름이 변경되었습니다.
이 PEP는 10년도 더 전에 출시된 Python 3.0을 대상으로 합니다. 누락된 기능을 추가하려는 모든 제안은 이 PEP의 구현을 완료하는 것이 아니라 새로운 기능으로 논의되어야 합니다.
개요 (Abstract)
이 PEP는 Python 3.0에서 메모리 공유 방식을 개선하기 위해 버퍼 인터페이스(PyBufferProcs 함수 포인터)를 재설계할 것을 제안합니다.
특히, API의 문자 버퍼(character buffer) 부분을 없애고, 여러 세그먼트(multiple-segment) 부분을 스트라이드(strided) 메모리 공유와 연계하여 재설계하는 것이 제안됩니다. 또한, 새로운 버퍼 인터페이스는 메모리의 다차원적 특성과 메모리가 포함하는 데이터 형식(data-format)을 공유할 수 있도록 할 것입니다.
이 인터페이스는 모든 확장 모듈(extension module)이 메모리를 공유하는 객체를 생성하거나, 인터페이스를 내보내는 임의의 객체에서 원시 메모리(raw memory)를 사용하고 조작하는 알고리즘을 생성할 수 있도록 합니다.
도입 배경 (Rationale)
Python 2.X 버퍼 프로토콜은 다른 Python 타입들이 내부 버퍼 시퀀스에 대한 포인터를 교환할 수 있도록 합니다. 이 기능은 서로 다른 고수준 객체들 간에 큰 메모리 세그먼트를 공유하는 데 매우 유용하지만, 너무 제한적이며 다음과 같은 문제가 있습니다.
- 잘 사용되지 않는 “세그먼트 시퀀스” 옵션(
bf_getsegcount
)이 있으며, 그 동기가 명확하지 않습니다. - 겉보기에 중복되는 문자 버퍼 옵션(
bf_getcharbuffer
)이 있습니다. - 소비자(consumer)가 버퍼 API를 내보내는 객체에 메모리 보기를 “다 사용했다”고 알릴 방법이 없으므로, 내보내는 객체가 자신이 소유한 메모리에 대한 포인터를 재할당하는 것이 안전하다고 확신할 방법이 없습니다. (예:
array
객체가 원본 포인터를 가지고 있던 버퍼 객체와 메모리를 공유한 후 메모리를 재할당하여 악명 높은 버퍼 객체 문제를 일으켰습니다.) - 메모리는 단순히 길이(length)를 가진 포인터일 뿐입니다. 메모리 “안에 무엇이 있는지”(float, int, C-구조체 등)를 설명할 방법이 없습니다.
- 메모리에 대한 형상(shape) 정보가 제공되지 않습니다. 하지만 여러 배열과 유사한 Python 타입(wxPython, GTK, pyQT, CVXOPT, PyVox, Audio and Video Libraries, ctypes, NumPy, 데이터베이스 인터페이스 등)은 메모리의 형상 해석(shape-interpretation)을 설명하는 표준적인 방법을 활용할 수 있습니다.
- 비연속적인 메모리(discontiguous memory)를 공유할 방법이 없습니다 (세그먼트 시퀀스 개념을 통하는 경우 제외).
비연속적인 메모리 개념을 사용하는 두 가지 널리 사용되는 라이브러리는 PIL과 NumPy입니다. 하지만 이들의 비연속 배열에 대한 관점은 다릅니다. 제안된 버퍼 인터페이스는 두 가지 메모리 모델 모두의 공유를 허용합니다. 내보내는 객체(Exporter)는 일반적으로 한 가지 접근 방식만 사용할 것이며, 소비자는 각 유형의 비연속 배열을 지원할지 여부를 선택할 수 있습니다.
NumPy는 각 차원에서 상수 스트라이딩(constant striding) 개념을 배열의 기본 개념으로 사용합니다. 이 개념을 통해 더 큰 배열의 간단한 하위 영역을 데이터를 복사하지 않고도 설명할 수 있습니다. 따라서 스트라이드 정보는 공유되어야 하는 추가 정보입니다.
PIL은 더 불투명한 메모리 표현을 사용합니다. 때로는 이미지가 연속적인 메모리 세그먼트에 포함되지만, 때로는 이미지의 연속적인 세그먼트(일반적으로 라인)에 대한 포인터 배열에 포함됩니다. PIL은 원래 버퍼 인터페이스에서 여러 버퍼 세그먼트 개념이 유래한 곳입니다.
NumPy의 스트라이드 메모리 모델은 계산 라이브러리에서 더 자주 사용되며, 그 단순성 때문에 이 모델을 사용한 메모리 공유를 지원하는 것이 합리적입니다. PIL 메모리 모델은 C-코드에서 2차원 배열을 이중 포인터 간접 참조(e.g. image[i][j]
)를 사용하여 접근할 수 있을 때 사용되기도 합니다.
버퍼 인터페이스는 객체가 이 두 가지 메모리 모델 중 하나를 내보낼 수 있도록 허용해야 합니다. 소비자는 연속적인 메모리를 요구하거나, 이 두 가지 메모리 모델 중 하나 또는 둘 다를 처리하는 코드를 자유롭게 작성할 수 있습니다.
제안 개요 (Proposal Overview)
- 버퍼 프로토콜의 문자 버퍼(char-buffer) 및 다중 세그먼트(multiple-segment) 섹션을 제거합니다.
- 버퍼를 가져오는 읽기/쓰기 버전을 통합합니다.
- 소비자 객체가 메모리 영역을 “다 사용했을” 때 호출되어야 하는 새로운 함수를 인터페이스에 추가합니다.
- 메모리에 무엇이 있는지 설명할 수 있도록 새로운 변수를 추가합니다 (현재
struct
및array
모듈에서 수행되는 작업을 통합). - 프로토콜이 형상(shape) 정보를 공유할 수 있도록 새로운 변수를 추가합니다.
- 스트라이드(stride) 정보를 공유하기 위한 새로운 변수를 추가합니다.
- 포인터 간접 참조(pointer indirection)를 사용하여 접근해야 하는 배열을 공유하기 위한 새로운 메커니즘을 추가합니다.
- 핵심(core) 및 표준 라이브러리(standard library)의 모든 객체가 새 인터페이스를 따르도록 수정합니다.
struct
모듈을 확장하여 더 많은 형식 지정자(format specifiers)를 처리할 수 있도록 합니다.- 버퍼 객체(buffer object)를 버퍼 인터페이스 위에 Python veneer를 씌운 새로운 메모리 객체(memory object)로 확장합니다.
- 버퍼 인터페이스를 지원하는 객체에서 연속적인 데이터(contiguous data)를 쉽게 복사하고 가져올 수 있도록 몇 가지 함수를 추가합니다.
사양 (Specification)
새로운 사양은 복잡한 메모리 공유를 허용하지만, 객체로부터 간단한 연속 바이트 버퍼를 여전히 얻을 수 있습니다. 사실, 새로운 프로토콜은 원본 객체가 연속적인 메모리 청크로 표현되지 않더라도 이를 수행하는 표준 메커니즘을 허용합니다.
가장 쉬운 방법은 제공된 C-API를 사용하여 메모리 청크를 얻는 것입니다.
PyBufferProcs
구조체를 다음과 같이 변경합니다.
typedef struct {
getbufferproc bf_getbuffer;
releasebufferproc bf_releasebuffer;
} PyBufferProcs;
이 두 루틴은 타입 객체(type object)에 대해 선택 사항입니다.
typedef int (*getbufferproc)(PyObject *obj, PyBuffer *view, int flags)
이 함수는 성공 시 0을 반환하고 실패 시 -1을 반환합니다 (그리고 오류를 발생시킵니다). 첫 번째 변수는 “내보내는(exporting)” 객체입니다. 두 번째 인수는 bufferinfo
구조체의 주소입니다. 두 인수 모두 NULL
이어서는 안 됩니다.
세 번째 인수는 소비자가 처리할 준비가 된 버퍼의 종류와 따라서 내보내는 객체가 반환할 수 있는 버퍼의 종류를 나타냅니다. 새로운 버퍼 인터페이스는 훨씬 더 복잡한 메모리 공유 가능성을 허용합니다. 일부 소비자는 모든 복잡성을 처리하지 못할 수 있지만, 내보내는 객체가 메모리에 대한 더 간단한 보기를 허용하는지 확인하고 싶을 수 있습니다.
또한, 일부 내보내는 객체는 모든 가능한 방식으로 메모리를 공유할 수 없으며, 일부 소비자에게 특정 작업이 불가능하다고 알리기 위해 오류를 발생시켜야 할 수도 있습니다. 이러한 오류는 실제로 문제를 일으키는 다른 오류가 없는 한 PyErr_BufferError
여야 합니다. 내보내는 객체는 flags
정보를 사용하여 PyBuffer
구조체의 얼마나 많은 요소가 기본값 이외의 값으로 채워지는지 단순화하거나, 객체가 메모리에 대한 더 간단한 보기를 지원할 수 없는 경우 오류를 발생시킬 수 있습니다.
내보내는 객체는 항상 버퍼 구조체의 모든 요소를 채워야 합니다 (요청된 것이 없으면 기본값 또는 NULL
로). PyBuffer_FillInfo
함수는 간단한 경우에 사용될 수 있습니다.
접근 플래그 (Access flags)
일부 플래그는 특정 종류의 메모리 세그먼트를 요청하는 데 유용하며, 다른 플래그는 소비자가 어떤 종류의 정보를 처리할 수 있는지 내보내는 객체에 알려줍니다. 만약 특정 정보가 소비자에 의해 요청되지 않았지만, 내보내는 객체가 그 정보 없이는 메모리를 공유할 수 없다면, PyErr_BufferError
가 발생해야 합니다.
PyBUF_SIMPLE
- 이것이 기본 플래그 상태(0)입니다. 반환된 버퍼는 쓰기 가능한 메모리를 가질 수도 있고 가지지 않을 수도 있습니다. 형식은 부호 없는 바이트(unsigned bytes)로 가정됩니다. 이것은 “독립적인(stand-alone)” 플래그 상수입니다. 다른 플래그와
|
로 연결할 필요가 없습니다. 내보내는 객체는 이러한 연속적인 바이트 버퍼를 제공할 수 없으면 오류를 발생시킵니다.
- 이것이 기본 플래그 상태(0)입니다. 반환된 버퍼는 쓰기 가능한 메모리를 가질 수도 있고 가지지 않을 수도 있습니다. 형식은 부호 없는 바이트(unsigned bytes)로 가정됩니다. 이것은 “독립적인(stand-alone)” 플래그 상수입니다. 다른 플래그와
PyBUF_WRITABLE
- 반환된 버퍼는 쓰기 가능해야 합니다. 쓰기 가능하지 않으면 오류를 발생시킵니다.
PyBUF_FORMAT
- 이 플래그가 제공되면 반환된 버퍼는 진정한 형식 정보를 가져야 합니다. 이것은 소비자가 실제로 저장된 데이터의 ‘종류’를 확인할 때 사용됩니다. 내보내는 객체는 요청 시 항상 이 정보를 제공할 수 있어야 합니다. 형식이 명시적으로 요청되지 않으면 형식은
NULL
로 반환되어야 합니다 (이는 “B”, 즉 부호 없는 바이트를 의미합니다).
- 이 플래그가 제공되면 반환된 버퍼는 진정한 형식 정보를 가져야 합니다. 이것은 소비자가 실제로 저장된 데이터의 ‘종류’를 확인할 때 사용됩니다. 내보내는 객체는 요청 시 항상 이 정보를 제공할 수 있어야 합니다. 형식이 명시적으로 요청되지 않으면 형식은
PyBUF_ND
- 반환된 버퍼는 형상(shape) 정보를 제공해야 합니다. 메모리는 C-스타일 연속(C-style contiguous)으로 가정됩니다 (마지막 차원이 가장 빠르게 변함). 내보내는 객체는 이러한 종류의 연속 버퍼를 제공할 수 없으면 오류를 발생시킬 수 있습니다. 이것이 주어지지 않으면
shape
는NULL
이 됩니다.
- 반환된 버퍼는 형상(shape) 정보를 제공해야 합니다. 메모리는 C-스타일 연속(C-style contiguous)으로 가정됩니다 (마지막 차원이 가장 빠르게 변함). 내보내는 객체는 이러한 종류의 연속 버퍼를 제공할 수 없으면 오류를 발생시킬 수 있습니다. 이것이 주어지지 않으면
PyBUF_STRIDES
(PyBUF_ND
를 암시)- 반환된 버퍼는 스트라이드(strides) 정보를 제공해야 합니다 (즉,
strides
는NULL
이 아니어야 합니다). 이것은 소비자가 스트라이드된(strided), 비연속적인 배열을 처리할 수 있을 때 사용됩니다. 스트라이드를 처리하는 것은 자동으로 형상(shape)을 처리할 수 있다고 가정합니다. 내보내는 객체는 (하위 오프셋(suboffsets) 없이) 데이터의 스트라이드된 표현만 제공할 수 없으면 오류를 발생시킬 수 있습니다.
- 반환된 버퍼는 스트라이드(strides) 정보를 제공해야 합니다 (즉,
PyBUF_C_CONTIGUOUS
PyBUF_F_CONTIGUOUS
PyBUF_ANY_CONTIGUOUS
- 이 플래그들은 반환된 버퍼가 각각 C-연속(C-contiguous, 마지막 차원이 가장 빠르게 변함), Fortran-연속(Fortran contiguous, 첫 번째 차원이 가장 빠르게 변함) 또는 둘 중 하나여야 함을 나타냅니다. 이 모든 플래그는
PyBUF_STRIDES
를 암시하며,strides
버퍼 정보 구조체가 올바르게 채워질 것을 보장합니다.
- 이 플래그들은 반환된 버퍼가 각각 C-연속(C-contiguous, 마지막 차원이 가장 빠르게 변함), Fortran-연속(Fortran contiguous, 첫 번째 차원이 가장 빠르게 변함) 또는 둘 중 하나여야 함을 나타냅니다. 이 모든 플래그는
PyBUF_INDIRECT
(PyBUF_STRIDES
를 암시)- 반환된 버퍼는 하위 오프셋(suboffsets) 정보를 가져야 합니다 (하위 오프셋이 필요하지 않은 경우
NULL
일 수 있습니다). 이것은 소비자가 이러한 하위 오프셋이 암시하는 간접 배열 참조를 처리할 수 있을 때 사용됩니다.
- 반환된 버퍼는 하위 오프셋(suboffsets) 정보를 가져야 합니다 (하위 오프셋이 필요하지 않은 경우
특정 종류의 메모리 공유를 위한 플래그의 특수 조합.
- 다차원 (하지만 연속적)
-
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
)
-
- 하위 오프셋을 사용하는 다차원
-
PyBUF_FULL
(PyBUF_INDIRECT
PyBUF_WRITABLE
PyBUF_FORMAT
) -
PyBUF_FULL_RO
(PyBUF_INDIRECT
PyBUF_FORMAT
)
-
따라서, 객체로부터 간단한 연속 바이트 청크를 원하는 소비자는 PyBUF_SIMPLE
을 사용할 수 있으며, 가장 복잡한 경우를 활용할 줄 아는 소비자는 PyBUF_FULL
을 사용할 수 있습니다.
PyBUF_FORMAT
이 flag
인수에 있는 경우에만 형식 정보가 NULL
이 아님을 보장하며, 그렇지 않은 경우 소비자는 부호 없는 바이트(unsigned bytes)로 가정할 것으로 예상됩니다.
연속적인 “부호 없는 바이트” 메모리 청크만 내보낼 수 있는 내보내는 객체를 위해 제공된 플래그에 따라 버퍼 정보 구조체를 올바르게 채우는 데 사용할 수 있는 C-API가 있습니다.
Py_buffer
구조체 (The Py_buffer struct)
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
로 종료되는 형식 문자열(확장 기능을 포함한struct
스타일 구문 따름)입니다. 요소의 수는len / itemsize
이며,itemsize
는 형식에 의해 암시되는 바이트 수입니다. 이것은NULL
일 수 있으며, 이 경우 표준 부호 없는 바이트(“B”)를 의미합니다.
- 메모리의 각 요소에 무엇이 들어있는지를 나타내는
ndim
- 메모리가 나타내는 차원의 수를 저장하는 변수입니다. 0 이상이어야 합니다. 0 값은
shape
,strides
,suboffsets
가NULL
이어야 함을 의미합니다 (즉, 메모리가 스칼라를 나타냅니다).
- 메모리가 나타내는 차원의 수를 저장하는 변수입니다. 0 이상이어야 합니다. 0 값은
shape
- 메모리의 형상(shape)을 N차원 배열로 나타내는
ndim
길이의Py_ssize_t
배열입니다.((*shape)[0] * ... * (*shape)[ndims-1])*itemsize = len
에 유의하십시오.ndim
이 0인 경우(스칼라를 나타냄) 이것은NULL
이어야 합니다.
- 메모리의 형상(shape)을 N차원 배열로 나타내는
strides
Py_ssize_t
길이의ndim
배열에 대한 포인터(또는ndim
이 0인 경우NULL
)로 채워질Py_ssize_t*
변수의 주소입니다. 각 차원에서 다음 요소로 이동하기 위해 건너뛸 바이트 수를 나타냅니다. 호출자가 이를 요청하지 않으면 (PyBUF_STRIDES
가 설정되지 않으면)NULL
로 설정되어 C-스타일 연속 배열을 나타내거나, 불가능한 경우PyExc_BufferError
가 발생해야 합니다.
suboffsets
Py_ssize_t
길이의*ndims
배열에 대한 포인터로 채워질Py_ssize_t*
변수의 주소입니다. 이suboffset
숫자가 0 이상인 경우, 지정된 차원을 따라 저장된 값은 포인터이며suboffset
값은 역참조(de-referencing) 후 포인터에 추가할 바이트 수를 지시합니다. 음수suboffset
값은 역참조가 발생하지 않아야 함을 나타냅니다 (연속 메모리 블록 내에서 스트라이딩). 모든suboffset
이 음수인 경우 (즉, 역참조가 필요 없음) 이것은NULL
이어야 합니다 (기본값). 호출자가 이를 요청하지 않으면 (PyBUF_INDIRECT
가 설정되지 않으면)NULL
로 설정되거나, 불가능한 경우PyExc_BufferError
가 발생해야 합니다.
명확성을 위해, strides
와 suboffsets
가 모두 NULL
이 아닐 때 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
번째 차원에서 슬라이싱(slicing)하면 (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
가 호출될 때 해제해야 합니다.
버퍼 해제 (Releasing the buffer)
동일한 bufferinfo
구조체가 release-buffer
인터페이스 호출에 사용되어야 합니다. 호출자는 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 호출 제안 (New C-API calls are proposed)
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)
- 버퍼 인터페이스를 정의하는 객체로부터
memory-view
객체를 반환합니다.
- 버퍼 인터페이스를 정의하는 객체로부터
memory-view
객체는 버퍼 객체를 대체할 수 있는 확장된 버퍼 객체입니다 (하지만 단순한 1차원 memory-view
객체로 유지될 수 있으므로 반드시 대체할 필요는 없습니다). 그 C-구조체는 다음과 같습니다.
typedef struct {
PyObject_HEAD
PyObject *base;
Py_buffer view;
} PyMemoryViewObject;
이것은 base
에 대한 참조가 유지되고 메모리 뷰가 다시 가져와지지 않는다는 점을 제외하면 현재 버퍼 객체와 기능적으로 유사합니다. 따라서 이 memory-view
객체는 삭제될 때까지 base
의 메모리를 유지합니다.
이 memory-view
객체는 다차원 슬라이싱(multi-dimensional slicing)을 지원하며, Python에서 이를 수행하는 최초의 객체가 될 것입니다. memory-view
객체의 슬라이스는 동일한 base
를 가지지만 base
객체에 대한 다른 뷰(view)를 가지는 다른 memory-view
객체입니다.
memory-view
에서 “요소”가 반환될 때마다 항상 bytes
객체이며, 그 형식은 memoryview
객체의 format
속성에 의해 해석되어야 합니다. 원한다면 struct
모듈을 사용하여 Python에서 바이트를 “디코딩”할 수 있습니다. 또는 내용을 NumPy 배열이나 버퍼 프로토콜을 사용하는 다른 객체에 전달할 수 있습니다.
Python 이름은 __builtin__.memoryview
가 될 것입니다.
메서드:
__getitem__
(다차원 슬라이싱 지원)__setitem__
(다차원 슬라이싱 지원)tobytes
(메모리 복사본의 새bytes
객체를 얻음).tolist
(메모리의 “중첩된” 리스트를 얻음.struct
모듈의unpack
이 하는 것처럼 모든 것이 표준 Python 객체로 해석됩니다. 실제로 이를 위해struct.unpack
을 사용합니다).
속성 (base
객체의 메모리에서 가져옴):
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
객체의base
객체가 됩니다.
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 객체에서 연속적인 메모리 영역으로 데이터를 가져오고 내보내는 표준적인 방법을 허용합니다. 이 호출들은 확장된 버퍼 인터페이스를 사용하여 작업을 수행합니다.
int PyBuffer_IsContiguous(Py_buffer *view, char fortran)
view
객체에 의해 정의된 메모리가 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)과 요소당 바이트 수(itemsize)를 가진 연속적인 배열(C-스타일,
fortran
이 ‘C’이면, 또는 Fortran-스타일,fortran
이 ‘F’이면)의 바이트-스트라이드로strides
배열을 채웁니다.
- 주어진 형상(shape)과 요소당 바이트 수(itemsize)를 가진 연속적인 배열(C-스타일,
int PyBuffer_FillInfo(Py_buffer *view, void *buf, Py_ssize_t len, int readonly, int infoflags)
- 주어진 길이의 “부호 없는 바이트” 연속 메모리 청크만 공유할 수 있는 내보내는 객체를 위해
buffer-info
구조체를 올바르게 채웁니다. 성공 시 0을 반환하고, 오류 시 -1을 반환합니다 (오류 발생 포함).
- 주어진 길이의 “부호 없는 바이트” 연속 메모리 청크만 공유할 수 있는 내보내는 객체를 위해
PyExc_BufferError
- 내보내는 객체가 소비자가 기대하는 종류의 버퍼를 제공할 수 없기 때문에 발생하는 버퍼 오류를 반환하기 위한 새로운 오류 객체입니다. 이 오류는 소비자가 프로토콜을 제공하지 않는 객체로부터 버퍼를 요청할 때도 발생합니다.
struct
문자열 구문 추가 사항 (Additions to the struct string-syntax
)
struct
문자열 구문에는 이미 다른 곳(예: ctypes
및 NumPy)에서 사용할 수 있는 데이터 형식 설명(data-format descriptions)을 완전히 구현하기 위한 일부 문자가 누락되어 있습니다. Python 2.5 사양은 http://docs.python.org/library/struct.html에 있습니다.
다음은 제안된 추가 사항입니다.
문자 | 설명 |
---|---|
't' |
비트 (앞에 붙는 숫자가 몇 비트인지 나타냄) |
'?' |
플랫폼 _Bool 타입 |
'g' |
long double |
'c' |
ucs-1 (latin-1) 인코딩 |
'u' |
ucs-2 |
'w' |
ucs-4 |
'O' |
Python 객체에 대한 포인터 |
'Z' |
복소수 (다음 지정자가 무엇이든) |
'&' |
특정 포인터 (다른 문자 앞에 붙는 접두사) |
'T{}' |
구조체 (내부에 상세 레이아웃 포함 {}) |
'(k1,k2,…,kn)' |
다음에 오는 무엇이든의 다차원 배열 |
':name:' |
이전 요소의 선택적 이름 |
'X{}' |
함수에 대한 포인터 (선택적 함수 시그니처 내부에 {} 포함, 반환 값은 -> 뒤에 붙고 끝에 위치) |
struct
모듈도 이러한 것들을 이해하고 언팩(unpacking) 시 적절한 Python 객체를 반환하도록 변경될 것입니다. long-double
을 언팩하면 decimal
객체 또는 ctypes
long-double
이 반환됩니다. 'u'
또는 'w'
를 언팩하면 Python unicode
가 반환됩니다. 다차원 배열을 언팩하면 리스트(1차원 이상인 경우 리스트의 리스트)가 반환됩니다. 포인터를 언팩하면 ctypes
포인터 객체가 반환됩니다. 함수 포인터를 언팩하면 ctypes
call-object
가 반환될 것입니다. 비트(bit)를 언팩하면 Python Bool
이 반환됩니다. struct
문자열 구문 내의 공백은 이미 무시되지 않는다면 무시될 것입니다. 명명된 객체(named-object)를 언팩하면 튜플처럼 작동하지만 항목에 이름으로도 접근할 수 있는 named-tuple
과 유사한 객체가 반환됩니다. 중첩된 구조체를 언팩하면 중첩된 튜플이 반환됩니다.
바이트 순서(Endian-specification) (!
, @
, =
, >
, <
, ^
)도 문자열 내에서 허용되므로 필요에 따라 변경될 수 있습니다. 이전에 지정된 바이트 순서 문자열은 변경될 때까지 유효합니다. 기본 바이트 순서는 ‘@’이며, 이는 네이티브 데이터 타입과 정렬을 의미합니다. 정렬되지 않은 네이티브 데이터 타입이 요청되면 바이트 순서 지정은 ‘^’입니다.
struct
모듈에 따르면, 숫자 코드 앞에 숫자가 붙어 해당 타입의 개수를 지정할 수 있습니다. (k1,k2,...,kn)
확장 기능은 데이터가 특정 형식의 (C-스타일 연속, 마지막 차원이 가장 빠르게 변함) 다차원 배열로 보여져야 하는지 여부도 지정할 수 있도록 합니다.
ctypes
에 struct
설명으로부터 ctypes
객체를 생성하고, long-double
및 ucs-2
를 ctypes
에 추가하는 함수가 추가되어야 합니다.
데이터 형식 설명 예시 (Examples of Data-Format Descriptions)
다음은 C-구조체의 예시와 struct
스타일 구문을 사용하여 어떻게 표현될 수 있는지 보여줍니다.
<named>
는 명명된 튜플(아직 지정되지 않음)의 생성자입니다.
float
'd'
<–> Pythonfloat
complex double
'Zd'
<–> Pythoncomplex
RGB Pixel data
'BBB'
<–>(int, int, int)
'B:r: B:g: B:b:'
<–><named>((int, int, int), ('r','g','b'))
Mixed endian
(이상하지만 가능)'>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차원 배열로 생각해야 한다고 전달하려는 사실과 무관합니다.
영향을 받는 코드 (Code to be affected)
이전 버퍼 인터페이스를 내보내거나 사용하는 Python의 모든 객체와 모듈이 수정될 것입니다. 다음은 부분적인 목록입니다.
buffer
객체bytes
객체string
객체unicode
객체array
모듈struct
모듈mmap
모듈ctypes
모듈
버퍼 API를 사용하는 다른 모든 것.
문제 및 세부 사항 (Issues and Details)
이 PEP는 기존 버퍼 프로토콜에 C-API와 두 가지 함수를 추가하여 Python 2.6으로 백포팅(back-ported)될 예정입니다.
이 PEP의 이전 버전에서는 읽기/쓰기 잠금(read/write locking) 체계를 제안했지만, 나중에 다음과 같이 인식되었습니다. a) 잠금이 필요 없는 일반적인 간단한 사용 사례에는 너무 복잡하고, b) 변경되고 짧게 유지되는 잠금을 사용하여 버퍼에 대한 동시 읽기/쓰기 접근이 필요한 사용 사례에는 너무 간단하다고 판단되었습니다. 따라서 동시 읽기/쓰기 접근에서 일관된 뷰(consistent views)가 필요한 경우 사용자가 버퍼 객체 주변에 자체적인 특정 잠금 체계를 구현하도록 맡겨져 있습니다. 이러한 사용자 체계에 대한 경험이 축적된 후에는 별도의 잠금 API를 포함하는 미래의 PEP가 제안될 수 있습니다.
스트라이드 메모리(strided memory)와 하위 오프셋(suboffsets)의 공유는 새로운 기능이며, 다중 세그먼트 인터페이스의 수정으로 볼 수 있습니다. 이는 NumPy와 PIL에서 영감을 받았습니다. NumPy 객체는 스트라이드 메모리를 관리하는 방법을 이해하는 코드와 스트라이드 메모리를 공유할 수 있어야 합니다. 왜냐하면 스트라이드 메모리는 계산 라이브러리와 인터페이스할 때 매우 흔하기 때문입니다.
또한, 이 접근 방식을 사용하면 복사 없이 두 가지 종류의 메모리 모두에서 작동하는 일반적인 코드를 작성할 수 있어야 합니다.
bufferinfo
구조체의 format
문자열, shape
배열, strides
배열, suboffsets
배열의 메모리 관리는 항상 내보내는 객체의 책임입니다. 소비자는 이러한 포인터를 다른 메모리로 설정하거나 해제하려고 시도해서는 안 됩니다.
몇 가지 아이디어가 논의되었고 거부되었습니다.
release-buffer
가 호출되는 “releaser” 객체를 두는 것. 이것은 프로토콜을 비대칭적으로 만들었기 때문에 (버퍼를 “얻은” 것과 다른 것에 대해 해제를 호출했음) 받아들일 수 없다고 판단되었습니다. 또한 실제 이점 없이 프로토콜을 복잡하게 만들었습니다.- 모든
struct
변수를 함수에 별도로 전달하는 것. 이것은 관심 없는 변수에NULL
을 설정할 수 있다는 장점이 있었지만, 함수 호출을 더 어렵게 만들었습니다.flags
변수는 소비자가 프로토콜을 호출하는 방식에서 “간단”할 수 있는 동일한 기능을 허용합니다.
코드 (Code)
PEP 작성자들은 이 제안에 대한 코드를 기여하고 유지 관리할 것을 약속하지만, 어떠한 도움도 환영할 것입니다.
코드 예시 (Code Examples)
예시 1 (Ex. 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
으로 할당된 struct rgba*
의 1차원 배열을 가리킵니다. 해당 블록의 각 포인터는 별도로 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 (Ex. 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 (Ex. 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 (Ex. 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);
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments