이 PEP는 현재 C 코드에서만 접근할 수 있는 버퍼 프로토콜을 위한 Python 수준 API를 제안합니다. 이를 통해 타입 검사기가 객체가 해당 프로토콜을 구현하는지 평가할 수 있습니다.
라이트 / 다크 / 자동 색상 테마 전환 PEP 688 – Python에서 버퍼 프로토콜에 접근 가능하게 만들기
저자:Jelle Zijlstra <jelle.zijlstra at gmail.com>논의:Discourse thread상태:최종 유형:표준 트랙 주제:Typing생성:23-Apr-2022 Python-버전:3.12 게시 이력:23-Apr-2022, 25-Apr-2022, 06-Oct-2022, 26-Oct-2022결의:07-Mar-2023
목차
중요
이 PEP는 역사적 문서입니다. 최신의 정본 문서는 이제 버퍼 타입 에뮬레이션에서 찾을 수 있습니다.
×
변경 제안 방법은 PEP 1을 참조하세요.
이 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 버퍼 프로토콜은 stride, 연속성, 버퍼 쓰기 지원 여부에 영향을 주는 많은 옵션을 지원합니다. 이 옵션들 중 일부는 타입 시스템에서 유용할 수 있습니다. 예를 들어 typeshed는 현재 쓰기 가능한 버퍼와 읽기 전용 버퍼에 대해 별도의 타입 별칭을 제공합니다.
하지만 C 버퍼 프로토콜에서는 이 옵션들 대부분을 타입 객체에서 직접 질의할 수 없습니다. 객체가 특정 플래그를 지원하는지 알아내는 유일한 방법은 실제로 버퍼를 요청해 보는 것입니다. memoryview 같은 일부 타입에서는 지원되는 플래그가 인스턴스에 따라 달라집니다. 결과적으로 타입 시스템에서 이러한 플래그 지원을 표현하기는 어려울 것입니다.
우리는 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로 감싸집니다.
__release_buffer__ 메서드는 호출자가 __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우리는 __buffer__ 메서드를 요구하는 새로운 추상 베이스 클래스 collections.abc.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.Iterable이나 collections.abc.Sized 같은 collections.abc의 다른 단순 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!"
새로운 typing 기능은 보통 typing_extensions 패키지에서 이전 Python 버전으로 백포트됩니다. 버퍼 프로토콜은 현재 C에서만 접근 가능하므로, 이 PEP는 typing_extensions 같은 순수 Python 패키지에서 완전히 구현될 수 없습니다. 임시 우회책으로, collections.abc.Buffer를 사용할 수 없는 Python 버전에는 추상 베이스 클래스 typing_extensions.Buffer가 제공됩니다.
이 PEP가 구현된 이후에는 객체가 버퍼 프로토콜을 지원함을 나타내기 위해 collections.abc.Buffer를 상속할 필요는 없습니다. 그러나 이전 Python 버전에서는 버퍼 프로토콜을 지원하는 객체에 __buffer__ 메서드가 없기 때문에, 타입 검사기에 클래스가 버퍼 프로토콜을 지원한다고 알리려면 typing_extensions.Buffer를 명시적으로 상속해야 합니다. 이는 주로 스텁 파일에서 일어날 것으로 예상됩니다. 버퍼 클래스는 필연적으로 C 코드로 구현되며, C 코드에는 타입을 인라인으로 정의할 수 없기 때문입니다. 런타임 용도에서는 ABC.register API를 사용해 버퍼 클래스를 typing_extensions.Buffer에 등록할 수 있습니다.
bytes에 대한 특별한 의미 없음다른 ByteString 타입의 축약형으로 bytes를 사용할 수 있다고 하는 특수 사례는 typing 문서에서 제거될 것입니다. 대안으로 collections.abc.Buffer를 사용할 수 있게 되면, bytes를 축약형으로 허용할 타당한 이유가 더 이상 없습니다. 현재 이 동작을 구현하는 타입 검사기는 이를 폐지 예정으로 표시하고 결국 제거해야 합니다.
__buffer__ 및 __release_buffer__ 속성이 PEP의 런타임 변경은 새로운 기능만 추가하므로, 하위 호환성 문제는 거의 없습니다.
하지만 __buffer__나 __release_buffer__ 속성을 다른 목적으로 사용하는 코드는 영향을 받을 수 있습니다. 모든 던더 이름은 기술적으로 언어를 위해 예약되어 있지만, 새로운 던더가 기존 코드, 특히 널리 사용되는 패키지와 지나치게 충돌하지 않도록 확인하는 것은 여전히 좋은 관행입니다. 공개적으로 접근 가능한 코드를 조사한 결과는 다음과 같습니다:
__buffer__ 메서드를 지원합니다. PyPy 코어 개발자 한 명이 이 PEP에 대한 지지 의사를 밝혔습니다.__buffer__ 메서드를 구현합니다.collections.abc.Buffer와 동등한 SupportsBuffer 프로토콜을 정의합니다.__buffer__ 속성(메서드 아님)에 접근하는 문서화되지 않은 동작이 있었습니다. 이는 2019년에 NumPy 1.17에서 제거되었습니다. 이 동작이 마지막으로 작동한 것은 NumPy 1.16이었을 텐데, 이 버전은 Python 3.7 이하만 지원했습니다. Python 3.7은 이 PEP가 구현될 것으로 예상되는 시점에는 이미 지원 종료에 도달해 있을 것입니다.따라서 이 PEP에서 __buffer__ 메서드를 사용하는 것은 PyPy와의 상호 운용성을 개선하고, 주요 Python 패키지의 현재 버전과도 충돌하지 않습니다.
공개적으로 접근 가능한 코드 중 __release_buffer__라는 이름을 사용하는 것은 없습니다.
bytes 특수 사례의 제거별도로, 타입 검사기에서 bytes의 특수 동작을 제거하라는 권고는 사용자에게 하위 호환성 영향을 줍니다. mypy를 사용한 실험에 따르면, 타입 검사에 이를 사용하는 몇몇 주요 오픈 소스 프로젝트는 bytes 승격을 제거하면 새로운 오류를 보게 됩니다. 이러한 오류 중 많은 것은 typeshed의 스텁을 개선함으로써 고칠 수 있으며, builtins, binascii, pickle, re 모듈에 대해서는 이미 그렇게 했습니다. typeshed에서 bytes 타입의 모든 사용을 검토하는 작업이 진행 중입니다. 전반적으로 이 변경은 타입 안전성을 개선하고 타입 시스템을 더 일관되게 만들므로, 우리는 마이그레이션 비용을 감수할 가치가 있다고 봅니다.
우리는 typing.python.org 및 mypy 치트 시트 같은 문서의 적절한 위치에 collections.abc.Buffer를 가리키는 주석을 추가할 것입니다. 타입 검사기는 오류 메시지에 추가 안내를 제공할 수도 있습니다. 예를 들어 버퍼 객체가 bytes만 받도록 애너테이션된 함수에 전달되는 상황을 만나면, 오류 메시지에 대신 collections.abc.Buffer 사용을 제안하는 메모를 포함할 수 있습니다.
이 PEP의 구현은 저자의 포크에서 사용 가능합니다.
types.Buffer이 PEP의 초기 버전에서는 새 types.Buffer 타입을 추가하고, 타입이 버퍼 프로토콜을 구현하는지 검사하기 위해 isinstance() 검사를 사용할 수 있도록 C로 구현된 __instancecheck__를 두는 방안을 제안했습니다. 이렇게 하면 전체 버퍼 프로토콜을 Python 코드에 노출하는 복잡성을 피하면서도, 타입 시스템이 버퍼 프로토콜을 검사할 수 있게 됩니다.
하지만 그 접근법은 타입 시스템의 나머지 부분과 잘 조합되지 않습니다. types.Buffer는 구조적 타입이 아니라 명목 타입이기 때문입니다. 예를 들어 “버퍼 프로토콜과 __len__을 모두 지원하는 객체”를 표현할 방법이 없습니다. 현재 제안에서는 __buffer__가 다른 어떤 특수 메서드와도 같으므로, 이를 다른 메서드와 결합한 Protocol을 정의할 수 있습니다.
더 일반적으로, Python의 다른 어떤 부분도 제안되었던 types.Buffer처럼 작동하지 않습니다. 현재 제안은 C 수준 슬롯에 대체로 대응하는 Python 수준 특수 메서드가 존재하는 언어의 나머지 부분과 더 일관적입니다.
bytearray를 bytes와 호환되게 유지memoryview가 항상 bytes와 호환된다는 특수 사례는 제거하되, 두 타입의 인터페이스가 매우 유사하므로 bytearray에 대해서는 유지하자는 제안이 있었습니다. 하지만 몇몇 표준 라이브러리 함수들(예: re.compile, socket.getaddrinfo, 그리고 path-like 인자를 받는 대부분의 함수들)은 bytes는 받지만 bytearray는 받지 않습니다. 또한 대부분의 코드베이스에서 bytearray는 그다지 흔한 타입도 아닙니다. 우리는 사용자가 허용되는 타입을 명시적으로 적거나(Protocol을 PEP 544에서 가져와 특정 메서드 집합만 필요할 때 사용할 수도 있습니다) 하는 편을 선호합니다. 이 제안의 이 측면은 typing-sig 메일링 리스트에서 구체적으로 논의되었으며, typing 커뮤니티에서 강한 반대는 없었습니다.
버퍼 타입에서 가장 자주 쓰이는 구분은 버퍼가 가변인지 아닌지입니다. 어떤 함수는 가변 버퍼만 받으며(예: bytearray, 일부 memoryview 객체), 다른 함수는 모든 버퍼를 받습니다.
이 PEP의 초기 버전에서는 버퍼 타입이 가변적인지를 결정하기 위해 bf_releasebuffer 슬롯의 존재를 사용하는 방안을 제안했습니다. 이 규칙은 대부분의 표준 라이브러리 버퍼 타입에 대해서는 성립하지만, 가변성과 이 슬롯의 존재 사이의 관계는 절대적이지 않습니다. 예를 들어 numpy 배열은 가변적이지만 이 슬롯이 없습니다.
현재 버퍼 프로토콜은 버퍼 타입이 가변 버퍼를 나타내는지 불변 버퍼를 나타내는지를 신뢰성 있게 판단할 방법을 제공하지 않습니다. 따라서 이 PEP는 이 구분에 대한 타입 시스템 지원을 추가하지 않습니다. 버퍼 프로토콜이 정적 인트로스펙션 지원을 제공하도록 향상된다면, 이 문제는 미래에 다시 검토할 수 있습니다. 그러한 메커니즘에 대한 개략안이 존재합니다.
많은 분들이 이 PEP 초안들에 유용한 피드백을 제공해 주셨습니다. Petr Viktorin은 버퍼 프로토콜의 미묘한 점들에 대한 제 이해를 높이는 데 특히 큰 도움을 주었습니다.
이 문서는 퍼블릭 도메인 또는 CC0-1.0-Universal 라이선스 중 더 허용적인 조건으로 제공됩니다.
출처: https://github.com/python/peps/blob/main/peps/pep-0688.rst
마지막 수정: 2025-03-05 16:28:34 GMT