PEP 688 – Python에서 버퍼 프로토콜에 접근 가능하게 하기
- Author:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 23-Apr-2022
- Python-Version:
- 3.12
- Post-History:
- 23-Apr-2022, 25-Apr-2022, 06-Oct-2022, 26-Oct-2022
- Resolution:
- 07-Mar-2023
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 현재 C 코드에서만 접근할 수 있는 버퍼 프로토콜을 위한 Python 수준 API를 제안합니다. 이를 통해 타입 검사기가 객체가 프로토콜을 구현하는지 평가할 수 있습니다.
동기
CPython C API는 객체의 기본 메모리에 접근하기 위한 다목적 메커니즘인 버퍼 프로토콜을 제공합니다. 이 프로토콜은 PEP 3118에서 도입되었습니다. 바이너리 데이터를 허용하는 함수는 일반적으로 버퍼 프로토콜을 구현하는 모든 객체를 처리하도록 작성됩니다. 예를 들어, 이 문서를 작성하는 시점에 CPython에는 버퍼 프로토콜을 허용하는 Argument Clinic Py_buffer 타입을 사용하는 함수가 약 130개 있습니다.
현재 Python 코드에서 객체가 버퍼 프로토콜을 지원하는지 검사할 방법은 없습니다. 또한 정적 타입 시스템은 프로토콜을 나타내는 타입 어노테이션을 제공하지 않습니다. 이는 일반 버퍼를 허용하는 코드의 타입 어노테이션을 작성할 때 발생하는 흔한 문제입니다.
마찬가지로 Python으로 작성된 클래스가 버퍼 프로토콜을 지원하는 것도 불가능합니다. Python의 버퍼 클래스는 사용자가 C 버퍼 객체를 쉽게 래핑하거나 버퍼 프로토콜을 사용하는 API의 동작을 테스트할 수 있게 합니다. 물론 이는 특히 흔한 요구 사항은 아닙니다. 그러나 Python으로 작성된 버퍼 클래스의 지원을 요청하는 CPython 기능 요청이 2012년부터 열려 있습니다.
근거
현재 선택지
타입 시스템에서 버퍼 타입에 어노테이션을 지정하기 위한 알려진 우회 방법은 두 가지가 있지만, 어느 것도 충분하지 않습니다.
첫째, typeshed에서 버퍼 타입에 사용하는 현재의 우회 방법은 표준 라이브러리에서 잘 알려진 버퍼 타입인 bytes, bytearray, memoryview, array.array를 나열하는 타입 별칭입니다. 이 방식은 표준 라이브러리에서는 작동하지만 서드 파티 버퍼 타입으로 확장되지는 않습니다.
둘째, typing.ByteString에 대한 문서는 현재 다음과 같이 설명합니다.
이 타입은 바이트 시퀀스의bytes,bytearray,memoryview타입을 나타냅니다.이 타입의 약식 표기로
bytes를 사용하여 위에 언급한 모든 타입의 인자에 어노테이션을 지정할 수 있습니다.
이 문장은 2015년부터 문서에 포함되어 있었지만, 이러한 다른 타입을 포함하도록 bytes를 사용하는 방식은 어떤 typing PEP에도 명시되어 있지 않습니다. 게다가 이 메커니즘에는 여러 문제가 있습니다. 모든 가능한 버퍼 타입을 포함하지 않으며, 타입 어노테이션에서 bytes 타입을 모호하게 만듭니다. 결국 bytes 객체에서는 유효하지만 memoryview 객체에서는 유효하지 않은 연산이 많으며, 함수가 bytes는 허용하지만 memoryview 객체는 허용하지 않는 것도 충분히 가능합니다. 한 mypy 사용자는 이 단축 표기로 인해 psycopg 프로젝트에 상당한 문제가 발생했다고 보고합니다.
버퍼의 종류
C 버퍼 프로토콜은 스트라이드, 연속성, 버퍼 쓰기 지원에 영향을 미치는 많은 옵션을 지원합니다. 이러한 옵션 중 일부는 타입 시스템에서 유용할 수 있습니다. 예를 들어, typeshed는 현재 쓰기 가능한 버퍼와 읽기 전용 버퍼에 대해 별도의 타입 별칭을 제공합니다.
그러나 C 버퍼 프로토콜에서는 이러한 옵션 대부분을 타입 객체에서 직접 조회할 수 없습니다. 객체가 특정 플래그를 지원하는지 확인하는 유일한 방법은 실제로 버퍼를 요청하는 것입니다. memoryview와 같은 일부 타입에서는 지원되는 플래그가 인스턴스에 따라 달라집니다. 따라서 타입 시스템에서 이러한 플래그에 대한 지원을 표현하기는 어려울 것입니다.
사양
Python 수준 버퍼 프로토콜
두 가지 Python 수준 특수 메서드인 __buffer__ 및 __release_buffer__를 추가할 것을 제안합니다. 이러한 메서드를 구현하는 Python 클래스는 C 코드에서 버퍼로 사용할 수 있습니다. 반대로 버퍼 프로토콜을 지원하도록 C로 구현된 클래스에는 Python 코드에서 접근할 수 있는 합성 메서드가 생성됩니다.
__buffer__ 메서드는 Python 객체에서 버퍼를 생성하기 위해 호출되며, 예를 들어 memoryview() 생성자가 호출합니다. 이는 bf_getbuffer C 슬롯에 대응합니다. 이 메서드의 Python 시그니처는 def __buffer__(self, flags: int, /) -> memoryview: ...입니다. 이 메서드는 memoryview 객체를 반환해야 합니다. __buffer__ 메서드가 있는 Python 클래스에서 bf_getbuffer 슬롯이 호출되면, 인터프리터는 메서드가 반환한 memoryview에서 기저 Py_buffer를 추출하여 C 호출자에게 반환합니다. 마찬가지로 bf_getbuffer를 구현하는 C 클래스의 인스턴스에서 Python 코드가 __buffer__ 메서드를 호출하면, 반환된 버퍼는 Python 코드에서 사용할 수 있도록 memoryview로 래핑됩니다.
호출자가 __buffer__가 반환한 버퍼를 더 이상 필요로 하지 않을 때 __release_buffer__ 메서드를 호출해야 합니다. 이는 bf_releasebuffer C 슬롯에 대응합니다. 이는 버퍼 프로토콜의 선택적 부분입니다. 이 메서드의 Python 시그니처는 def __release_buffer__(self, buffer: memoryview, /) -> None: ...입니다. 해제할 버퍼는 memoryview로 래핑됩니다. 이 메서드가 CPython의 버퍼 API를 통해 호출될 때(예를 들어 __buffer__가 반환한 memoryview에서 memoryview.release를 호출할 때), 전달되는 memoryview는 __buffer__가 반환한 객체와 동일한 객체입니다. bf_releasebuffer를 구현하는 C 클래스에서 __release_buffer__를 호출하는 것도 가능합니다.
객체에 __release_buffer__가 존재하는 경우, 객체에서 직접 __buffer__를 호출하는 Python 코드는 버퍼 사용을 마쳤을 때 동일한 객체에서 __release_buffer__를 호출해야 합니다. 그렇지 않으면 객체가 사용하는 리소스가 회수되지 않을 수 있습니다. 마찬가지로 __buffer__를 이전에 호출하지 않고 __release_buffer__를 호출하거나, __buffer__를 한 번 호출한 것에 대해 이를 여러 번 호출하는 것은 프로그래밍 오류입니다. C 버퍼 프로토콜을 구현하는 객체의 경우, 인자가 동일한 객체를 래핑하는 memoryview가 아닌 상태에서 __release_buffer__를 호출하면 예외가 발생합니다. __release_buffer__를 유효하게 호출한 후에는 memoryview가 무효화되며(해당 release() 메서드가 호출된 것처럼 동작함), 동일한 memoryview를 사용하여 이후에 __release_buffer__를 호출하면 예외가 발생합니다. 인터프리터는 Python API의 오용으로 인해 C 수준의 불변 조건이 깨지지 않도록 보장합니다. 예를 들어 메모리 안전성 위반이 발생하지 않습니다.
inspect.BufferFlags
__buffer__의 구현을 지원하기 위해 enum.IntFlag의 서브클래스인 inspect.BufferFlags를 추가합니다. 이 열거형에는 C 버퍼 프로토콜에 정의된 모든 플래그가 포함됩니다. 예를 들어 inspect.BufferFlags.SIMPLE은 PyBUF_SIMPLE 상수와 동일한 값을 가집니다.
collections.abc.Buffer
새로운 추상 베이스 클래스인 collections.abc.Buffer를 추가하며, __buffer__메서드를 요구합니다. 이 클래스는 주로 타입 어노테이션에 사용하기 위한 것입니다:
def need_buffer(b: Buffer) -> memoryview:
return memoryview(b)
need_buffer(b"xy") # ok
need_buffer("xy") # rejected by static type checkers
isinstance 및 issubclass 검사에도 사용할 수 있습니다:
>>> from collections.abc import Buffer
>>> isinstance(b"xy", Buffer)
True
>>> issubclass(bytes, Buffer)
True
>>> issubclass(memoryview, Buffer)
True
>>> isinstance("xy", Buffer)
False
>>> issubclass(str, Buffer)
False
typeshed 스텁 파일에서는 collections.abc의 collections.abc.Iterable이나 collections.abc.Sized와 같은 다른 간단한 ABC의 선례에 따라 이 클래스를 Protocol로 정의해야 합니다.
예시
다음은 버퍼 프로토콜을 구현하는 Python 클래스의 예시입니다:
import contextlib
import inspect
class MyBuffer:
def __init__(self, data: bytes):
self.data = bytearray(data)
self.view = None
def __buffer__(self, flags: int) -> memoryview:
if flags != inspect.BufferFlags.FULL_RO:
raise TypeError("Only BufferFlags.FULL_RO supported")
if self.view is not None:
raise RuntimeError("Buffer already held")
self.view = memoryview(self.data)
return self.view
def __release_buffer__(self, view: memoryview) -> None:
assert self.view is view # guaranteed to be true
self.view.release()
self.view = None
def extend(self, b: bytes) -> None:
if self.view is not None:
raise RuntimeError("Cannot extend held buffer")
self.data.extend(b)
buffer = MyBuffer(b"capybara")
with memoryview(buffer) as view:
view[0] = ord("C")
with contextlib.suppress(RuntimeError):
buffer.extend(b"!") # raises RuntimeError
buffer.extend(b"!") # ok, buffer is no longer held
with memoryview(buffer) as view:
assert view.tobytes() == b"Capybara!"
이전 Python 버전에서의 동등한 방법
새로운 타이핑 기능은 일반적으로 typing_extensions 패키지에서 이전 Python 버전으로 백포트됩니다. 버퍼 프로토콜은 현재 C에서만 접근할 수 있으므로, 이 PEP는 typing_extensions와 같은 순수 Python 패키지에서 완전히 구현할 수 없습니다. 임시 해결 방법으로 collections.abc.Buffer를 사용할 수 없는 Python 버전에 typing_extensions.Buffer라는 추상 베이스 클래스를 제공합니다.
이 PEP가 구현된 후에는 객체가 버퍼 프로토콜을 지원함을 나타내기 위해 collections.abc.Buffer를 상속할 필요가 없습니다. 그러나 이전 Python 버전에서는 클래스가 버퍼 프로토콜을 지원함을 타입 검사기에 나타내려면 typing_extensions.Buffer를 명시적으로 상속해야 합니다. 버퍼 프로토콜을 지원하는 객체에는 __buffer__메서드가 없기 때문입니다. 버퍼 클래스는 반드시 C 코드로 구현되며 C 코드에는 타입을 인라인으로 정의할 수 없으므로, 이는 주로 스텁 파일에서 이루어질 것으로 예상됩니다. 런타임 용도에서는 ABC.register API를 사용하여 버퍼 클래스를 typing_extensions.Buffer에 등록할 수 있습니다.
bytes에 특별한 의미 없음
bytes를 다른 ByteString 타입의 약칭으로 사용할 수 있다고 명시한 특례는 typing 문서에서 제거됩니다. collections.abc.Buffer를 대안으로 사용할 수 있으므로 bytes를 약칭으로 허용할 타당한 이유가 없습니다. 현재 이 동작을 구현하는 타입 검사기는 해당 기능을 사용 중단 대상으로 지정하고 결국 제거해야 합니다.
하위 호환성
__buffer__ 및 __release_buffer__ 속성
이 PEP의 런타임 변경 사항은 새로운 기능만 추가하므로 하위 호환성 우려는 거의 없습니다.
그러나 __buffer__ 또는 __release_buffer__ 속성을 다른 목적으로 사용하는 코드에는 영향이 있을 수 있습니다. 모든 던더가 기술적으로 언어를 위해 예약되어 있지만, 새로운 던더가 기존 코드, 특히 널리 사용되는 패키지와 과도하게 충돌하지 않도록 하는 것은 여전히 좋은 관행입니다. 공개적으로 접근 가능한 코드를 조사한 결과 다음이 발견되었습니다:
- PyPy는 supports하며, 이 PEP에서 제안한 것과 호환되는 의미의
__buffer__메서드를 지원합니다. PyPy 핵심 개발자는 expressed his support를 통해 이 PEP에 대한 지지를 표명했습니다. - pyzmq는 implements하는 PyPy 호환
__buffer__메서드를 구현합니다. - mpi4py는 defines하는
SupportsBuffer프로토콜을 정의하며, 이는 이 PEP의collections.abc.Buffer와 동등합니다. - NumPy에는 객체의 버퍼를 가져오기 위해
__buffer__속성(메서드가 아님)에 액세스하는 문서화되지 않은 동작이 있었습니다. 이는 2019년에 NumPy 1.17에서 removed되었습니다. 이 동작은 Python 3.7 이하만 지원했던 NumPy 1.16에서 마지막으로 작동했을 것입니다. 이 PEP가 구현될 것으로 예상되는 시점에는 Python 3.7의 지원 종료 시점이 지났을 것입니다.
따라서 이 PEP에서 __buffer__ 메서드를 사용하면 PyPy와의 상호 운용성이 향상되며, 주요 Python 패키지의 현재 버전과 충돌하지 않습니다.
공개적으로 액세스할 수 있는 코드에서는 __release_buffer__라는 이름을 사용하지 않습니다.
bytes 특수 사례 제거
이와 별도로, 타입 검사기에서 bytes에 대한 특수 동작을 제거하라는 권고는 사용자에게 하위 호환성 영향을 미칩니다. mypy를 사용한 experiment에 따르면, 타입 검사에 이를 사용하는 여러 주요 오픈 소스 프로젝트에서 bytes 승격이 제거되면 새로운 오류가 발생합니다. 이러한 오류 중 상당수는 typeshed의 스텁을 개선하여 수정할 수 있으며, 이는 이미 builtins, binascii, pickle, re 모듈에 대해 수행되었습니다. typeshed에서 bytes 유형의 모든 사용을 review하는 작업이 진행 중입니다. 전반적으로 이 변경은 타입 안전성을 향상하고 타입 시스템을 더욱 일관되게 만들므로, 마이그레이션 비용을 감수할 가치가 있다고 생각합니다.
이 내용을 가르치는 방법
typing.python.org 및 mypy cheat sheet와 같은 문서의 적절한 위치에 collections.abc.Buffer를 가리키는 참고 사항을 추가할 예정입니다. 타입 검사기는 오류 메시지에 추가 안내를 제공할 수도 있습니다. 예를 들어 버퍼 객체가 bytes만 허용하도록 어노테이션된 함수에 전달되는 상황을 발견하면, 오류 메시지에 대신 collections.abc.Buffer를 사용하라는 참고 사항을 포함할 수 있습니다.
참조 구현
이 PEP의 구현은 작성자의 포크에서 available합니다.
거부된 아이디어
types.Buffer
이 PEP의 이전 버전에서는 C로 구현된 __instancecheck__를 포함하는 새로운 types.Buffer 유형을 추가하여, isinstance() 검사를 사용해 유형이 버퍼 프로토콜을 구현하는지 확인할 수 있도록 제안했습니다. 이렇게 하면 전체 버퍼 프로토콜을 Python 코드에 노출하는 복잡성을 피하면서도 타입 시스템이 버퍼 프로토콜을 검사할 수 있습니다.
그러나 types.Buffer는 구조적 유형이 아닌 명목 유형이 되므로 이 접근 방식은 나머지 타입 시스템과 잘 조합되지 않습니다. 예를 들어 버퍼 프로토콜과 __len__을 모두 지원하는 객체를 표현할 방법이 없습니다. 현재 제안에서는 __buffer__가 다른 특수 메서드와 마찬가지이므로, 다른 메서드와 결합하는 Protocol을 정의할 수 있습니다.
더 일반적으로 말해, 제안된 types.Buffer와 같은 방식으로 작동하는 Python의 다른 부분은 없습니다. C 수준 슬롯에는 대개 이에 대응하는 Python 수준 특수 메서드가 있는 언어의 나머지 부분과 비교할 때, 현재 제안이 더 일관됩니다.
bytearray를 bytes와 호환되도록 유지하십시오.
memoryview가 항상 bytes와 호환되는 특수 사례를 제거하되, 두 타입의 인터페이스가 매우 유사하므로 bytearray에 대해서는 유지하자는 제안이 있었습니다. 그러나 여러 표준 라이브러리 함수(예: re.compile, socket.getaddrinfo 및 경로 유사 인자를 허용하는 대부분의 함수)는 bytes는 허용하지만 bytearray는 허용하지 않습니다. 또한 대부분의 코드베이스에서 bytearray는 매우 흔한 타입이 아닙니다. 사용자가 허용되는 타입을 명시적으로 작성하도록 하는 방식을 선호합니다(특정 메서드 집합만 필요한 경우에는 PEP 544의 Protocol을 사용하십시오). 이 제안의 해당 측면은 typing-sig 메일링 리스트에서 구체적으로 논의되었습니다에서, typing 커뮤니티의 강한 이견은 없었습니다.
변경 가능한 버퍼와 변경 불가능한 버퍼를 구분합니다
버퍼 타입에서 가장 자주 사용되는 구분은 버퍼가 변경 가능한지 여부입니다. 일부 함수는 변경 가능한 버퍼만 허용하고(예: bytearray, 일부 memoryview객체), 다른 함수는 모든 버퍼를 허용합니다.
이 PEP의 이전 버전에서는 버퍼 타입이 변경 가능한지 판별하기 위해 bf_releasebuffer슬롯의 존재 여부를 사용하도록 제안했습니다. 이 규칙은 대부분의 표준 라이브러리 버퍼 타입에 적용되지만, 변경 가능성과 이 슬롯의 존재 사이의 관계가 절대적인 것은 아닙니다. 예를 들어 numpy배열은 변경 가능하지만 이 슬롯을 갖지 않습니다.
현재 버퍼 프로토콜은 버퍼 타입이 변경 가능한 버퍼를 나타내는지 또는 변경 불가능한 버퍼를 나타내는지 안정적으로 판별할 방법을 제공하지 않습니다. 따라서 이 PEP는 이러한 구분을 위한 타입 시스템 지원을 추가하지 않습니다. 버퍼 프로토콜이 정적 인트로스펙션 지원을 제공하도록 향상된다면 향후 이 문제를 다시 검토할 수 있습니다. 이러한 메커니즘에 대한 개요가 존재합니다.
감사의 말
많은 분이 이 PEP의 초안에 유용한 피드백을 제공해 주셨습니다. Petr Viktorin은 버퍼 프로토콜의 미묘한 부분에 대한 저의 이해를 향상하는 데 특히 큰 도움을 주셨습니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.