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. 메모리에 대한 형태(shape) 정보가 제공되지 않습니다. 하지만 여러 배열 유사 Python 타입은 메모리의 형태 해석을 기술하는 표준 방식을 활용할 수 있습니다(wxPython, GTK, pyQT, CVXOPT, PyVox, 오디오 및 비디오 라이브러리, ctypes, NumPy, 데이터베이스 인터페이스 등).
  6. (세그먼트 시퀀스 개념을 통하는 것 외에는) 불연속 메모리를 공유할 방법이 없습니다.

    불연속 메모리 개념을 사용하는 널리 쓰이는 라이브러리로 PIL과 NumPy 두 가지가 있습니다. 하지만 두 라이브러리가 불연속 배열을 바라보는 관점은 서로 다릅니다. 제안하는 버퍼 인터페이스는 두 메모리 모델 모두를 공유할 수 있게 합니다. 내보내는 쪽은 일반적으로 한 가지 방식만 사용할 것이며, 소비자는 각 유형의 불연속 배열을 원하는 방식으로 지원하도록 선택할 수 있습니다.

    NumPy는 각 차원에서의 일정한 상수 스트라이드(striding) 개념을 배열의 기본 개념으로 사용합니다. 이 개념을 사용하면 더 큰 배열의 단순한 하위 영역을 데이터 복사 없이 기술할 수 있습니다. 따라서 스트라이드 정보가 추가로 공유되어야 하는 정보입니다.

    PIL은 더 불투명한 메모리 표현을 사용합니다. 이미지가 연속된 메모리 세그먼트 하나에 담겨 있을 때도 있지만, 이미지의 연속된 세그먼트(보통은 행)를 가리키는 포인터 배열에 담겨 있을 때도 있습니다. 원래 버퍼 인터페이스에 있던 다중 버퍼 세그먼트라는 아이디어는 PIL에서 나왔습니다.

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

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

제안 개요

  • 버퍼 프로토콜에서 문자 버퍼(char-buffer)와 다중 세그먼트 부분을 제거합니다.
  • 버퍼를 얻는 읽기 버전과 쓰기 버전을 하나로 통합합니다.
  • 소비자 객체가 메모리 영역을 다 “사용했을” 때 호출해야 하는 새 함수를 인터페이스에 추가합니다.
  • 인터페이스가 메모리에 무엇이 있는지 기술할 수 있도록 새 변수를 추가합니다(현재 struct와 배열에서 각각 이루어지고 있는 것을 통합합니다).
  • 프로토콜이 형상(shape) 정보를 공유할 수 있도록 새 변수를 추가합니다.
  • 스트라이드 정보를 공유하기 위한 새 변수를 추가합니다.
  • 포인터 간접 참조로 접근해야 하는 배열을 공유하기 위한 새 메커니즘을 추가합니다.
  • 코어와 표준 라이브러리의 모든 객체를 새 인터페이스에 맞게 수정합니다.
  • 더 많은 형식 지정자를 처리할 수 있도록 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)입니다. 반환된 버퍼의 메모리는 쓰기 가능할 수도 있고 그렇지 않을 수도 있습니다. 형식은 부호 없는 바이트로 간주됩니다. 이것은 “독립형” 플래그 상수입니다. 다른 플래그와 | 연산으로 결합할 필요가 전혀 없습니다. 익스포터가 이러한 연속된 바이트 버퍼를 제공할 수 없으면 오류를 발생시킵니다.

PyBUF_WRITABLE

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

PyBUF_FORMAT

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

PyBUF_ND

반환된 버퍼는 형태(shape) 정보를 제공해야 합니다. 메모리는 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 정보를 가져야 합니다(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이 있을 때만 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로 끝나는 형식 문자열(확장을 포함한 struct 스타일 문법을 따릅니다)입니다. 요소의 개수는 len / itemsize이며, 여기서 itemsize는 형식이 암시하는 바이트 수입니다. 이 값은 NULL일 수 있으며, 이 경우 표준 부호 없는 바이트(“B”)를 의미합니다.
ndim
메모리가 나타내는 차원의 수를 저장하는 변수입니다. 0 이상이어야 합니다. 값이 0이면 shape, strides, suboffsets가 모두 NULL이어야 함을 의미합니다(즉, 메모리가 스칼라를 나타냅니다).
shape
메모리를 N차원 배열로 보았을 때의 형태를 나타내는, 길이가 ndims인 Py_ssize_t 배열입니다. ((*shape)[0] * ... * (*shape)[ndims-1])*itemsize = len임에 유의하십시오. ndims가 0이면(스칼라를 나타냄) 이 값은 NULL이어야 합니다.
strides
길이가 ndims인 Py_ssize_t 배열(ndims가 0이면 NULL)을 가리키는 포인터로 채워질 Py_ssize_t* 변수의 주소입니다. 이 배열은 각 차원에서 다음 요소로 가기 위해 건너뛰어야 하는 바이트 수를 나타냅니다. 호출자가 이를 요청하지 않은 경우(PyBUF_STRIDES가 설정되지 않은 경우) 이 값은 C 스타일 연속 배열을 나타내는 NULL로 설정되어야 하며, 이것이 불가능하면 PyExc_BufferError가 발생해야 합니다.
suboffsets
길이가 *ndims인 Py_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 배열을 해제해야 하는지에 대한 플래그를 저장하는 데 사용할 수 있습니다. 소비자는 이 값을 절대 변경해서는 안 됩니다.

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

버퍼 해제

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

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

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

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)

버퍼 인터페이스를 정의하는 객체로부터 메모리 뷰(memory-view) 객체를 반환합니다.

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

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

이는 base에 대한 참조가 유지되고 메모리 뷰를 다시 가져오지 않는다는 점을 제외하면 현재의 버퍼 객체와 기능적으로 유사합니다. 따라서 이 memory view 객체는 삭제될 때까지 base의 메모리를 붙잡고 있습니다.

이 메모리 뷰 객체는 다차원 슬라이싱을 지원할 것이며, Python에서 제공되는 객체 중 이를 지원하는 최초의 객체가 될 것입니다. 메모리 뷰 객체의 슬라이스는 같은 base를 가지지만 base 객체를 다르게 바라보는 또 다른 메모리 뷰 객체입니다.

메모리 뷰에서 “요소”가 반환될 때 그것은 항상 bytes 객체이며, 그 형식은 memoryview 객체의 format 속성에 따라 해석되어야 합니다. 원한다면 struct 모듈을 사용하여 Python에서 바이트를 “디코딩”할 수 있습니다. 또는 그 내용을 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을 사용합니다).

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

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

struct 스타일의 기술(description)로부터 데이터 형식 영역의 암묵적인 itemsize를 반환합니다.

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

obj가 나타내는 연속된 메모리 덩어리에 대한 memoryview 객체를 반환합니다. (obj가 가리키는 메모리가 연속적이지 않아서) 복사본을 만들어야 하는 경우, 새 bytes 객체가 생성되어 반환되는 memoryview 객체의 베이스 객체가 됩니다.

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)

뷰 객체가 정의하는 메모리가 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 스타일)의 바이트 단위 stride로 strides 배열을 채웁니다.

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

주어진 길이의 “unsigned bytes”로 이루어진 연속된 메모리 덩어리만 공유할 수 있는 내보내는 쪽(exporter)을 위해 버퍼 정보(buffer-info) 구조체를 올바르게 채웁니다. 성공하면 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 문자열 문법의 공백은 아직 무시되지 않는다면 무시되도록 합니다. 이름 있는 객체를 언패킹하면 튜플처럼 동작하면서도 항목에 이름으로 접근할 수 있는 일종의 named-tuple 같은 객체를 반환합니다. 중첩된 구조체를 언패킹하면 중첩된 튜플을 반환합니다.

엔디언 명세(‘!’, ‘@’,’=’,’>’,’<’, ‘^’)도 문자열 안에서 허용되므로 필요하면 엔디언을 변경할 수 있습니다. 앞서 지정한 엔디언 문자열은 바뀔 때까지 유효합니다. 기본 엔디언은 ‘@’이며, 네이티브 데이터 타입과 정렬을 뜻합니다. 정렬되지 않은 네이티브 데이터 타입이 요청되면 엔디언 명세는 ‘^’입니다.

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

ctypes에 struct 기술로부터 ctypes 객체를 만드는 함수를 추가하고, ctypes에 long-double과 ucs-2를 추가해야 합니다.

데이터 형식 기술의 예

다음은 C 구조체 몇 가지와 그것을 struct 스타일 문법으로 나타내는 방법을 보여 주는 예입니다.

<named>는 이름 있는 튜플(named-tuple)의 생성자입니다(아직 명세되지 않았습니다).

float
'd' <–> Python float
complex 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의 모든 객체와 모듈이 수정됩니다. 다음은 일부 목록입니다.

  • 버퍼 객체
  • bytes 객체
  • 문자열 객체
  • 유니코드 객체
  • 배열 모듈
  • struct 모듈
  • mmap 모듈
  • ctypes 모듈

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

문제점 및 세부 사항

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

이 PEP의 이전 버전은 읽기/쓰기 잠금 방식을 제안했지만, 이후 이 방식이 a) 잠금이 전혀 필요 없는 일반적이고 단순한 사용 사례에는 너무 복잡하고, b) 수시로 바뀌는 단기 잠금으로 버퍼에 동시 읽기/쓰기 접근이 필요한 사용 사례에는 너무 단순하다고 판단되었습니다. 따라서 동시 읽기/쓰기 접근 중에도 일관된 뷰가 필요한 사용자는 버퍼 객체를 둘러싼 자신만의 잠금 방식을 직접 구현해야 합니다. 이러한 사용자 방식에 관한 경험이 어느 정도 쌓이면, 별도의 잠금 API를 포함하는 향후 PEP가 제안될 수 있습니다.

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

또한 이 방식을 사용하면 복사 없이 두 종류의 메모리 모두에서 동작하는 범용 코드를 작성할 수 있어야 합니다.

bufferinfo 구조체에 있는 형식 문자열, shape 배열, strides 배열, suboffsets 배열의 메모리 관리는 항상 내보내는 객체가 책임집니다. 소비자는 이 포인터들을 다른 메모리로 설정하거나 해제하려고 해서는 안 됩니다.

여러 아이디어가 논의되었으나 기각되었습니다:

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

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

코드

이 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으로 할당된 (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

이 예제는 연속된 메모리 덩어리(객체가 살아 있는 동안에는 절대 재할당되지 않음)를 노출하려는 객체가 이를 수행하는 방법을 보여 줍니다.

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