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

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at Emulating buffer types.

×

See PEP 1 for how to propose changes.

초록

이 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를 사용하는 방식은 어떤 타이핑 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 호출자에게 반환합니다. 마찬가지로, Python 코드가 bf_getbuffer를 구현하는 C 클래스의 인스턴스에 대해 __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!"

이전 Python 버전을 위한 대체 수단

새로운 타이핑 기능은 보통 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의 특별한 의미 제거

bytes를 다른 ByteString 타입의 약칭으로 사용할 수 있다는 특수 사례는 typing 문서에서 제거됩니다. collections.abc.Buffer를 대안으로 사용할 수 있으므로, bytes를 약칭으로 허용할 타당한 이유가 없어집니다. 현재 이 동작을 구현하고 있는 타입 검사기는 이를 폐기 예정으로 표시하고 최종적으로 제거해야 합니다.

하위 호환성

__buffer__ 및 __release_buffer__ 속성

이 PEP의 런타임 변경 사항은 새로운 기능을 추가할 뿐이므로 하위 호환성 문제는 거의 없습니다.

그러나 __buffer__ 또는 __release_buffer__ 속성을 다른 용도로 사용하는 코드는 영향을 받을 수 있습니다. 기술적으로 모든 던더는 언어를 위해 예약되어 있지만, 새로운 던더가 너무 많은 기존 코드, 특히 널리 사용되는 패키지와 충돌하지 않도록 하는 것은 여전히 바람직한 관행입니다. 공개적으로 접근 가능한 코드를 조사한 결과는 다음과 같습니다:

  • PyPy는 이 PEP에서 제안하는 것과 호환되는 의미를 가진 __buffer__ 메서드를 지원합니다. 한 PyPy 코어 개발자는 이 PEP에 대한 지지를 표명했습니다.
  • pyzmq는 PyPy와 호환되는 __buffer__ 메서드를 구현합니다.
  • mpi4py는 이 PEP의 collections.abc.Buffer와 동등한 SupportsBuffer 프로토콜을 정의합니다.
  • NumPy에는 과거에 객체의 버퍼를 얻기 위해 (메서드가 아닌) __buffer__ 속성에 접근하는, 문서화되지 않은 동작이 있었습니다. 이 동작은 2019년 NumPy 1.17에서 제거되었습니다. 이 동작이 마지막으로 작동했을 버전은 NumPy 1.16이며, 이 버전은 Python 3.7 이하만 지원했습니다. 이 PEP가 구현될 것으로 예상되는 시점에는 Python 3.7의 지원이 이미 종료되었을 것입니다.

따라서 이 PEP에서 __buffer__ 메서드를 사용하면 PyPy와의 상호 운용성이 개선되며, 주요 Python 패키지의 현재 버전 어느 것에도 지장을 주지 않습니다.

공개적으로 접근 가능한 코드 중 __release_buffer__라는 이름을 사용하는 코드는 없습니다.

bytes 특수 사례 제거

이와 별개로, 타입 검사기에서 bytes에 대한 특수 동작을 제거하라는 권고는 타입 검사기 사용자에게 하위 호환성 측면의 영향을 미칩니다. mypy로 수행한 실험에 따르면, 타입 검사에 mypy를 사용하는 여러 주요 오픈 소스 프로젝트에서 bytes 승격이 제거되면 새로운 오류가 발생합니다. 이러한 오류 중 상당수는 typeshed의 스텁을 개선하여 수정할 수 있으며, 이는 이미 builtins, binascii, pickle, re 모듈에 대해 이루어졌습니다. typeshed에서 bytes 타입이 사용되는 모든 곳에 대한 검토가 진행 중입니다. 전반적으로 이 변경은 타입 안전성을 개선하고 타입 시스템을 더 일관성 있게 만들므로, 마이그레이션 비용을 들일 만한 가치가 있다고 생각합니다.

교육 방법

typing.python.org와 mypy 치트 시트 등 문서의 적절한 위치에 collections.abc.Buffer를 안내하는 참고 사항을 추가할 것입니다. 타입 검사기는 오류 메시지에 추가적인 안내를 제공할 수 있습니다. 예를 들어 bytes만 받도록 어노테이션된 함수에 버퍼 객체가 전달되는 경우, 오류 메시지에 collections.abc.Buffer를 대신 사용하도록 제안하는 참고 사항을 포함할 수 있습니다.

참조 구현

이 PEP의 구현은 저자의 포크에서 확인할 수 있습니다.

기각된 아이디어

types.Buffer

이 PEP의 이전 버전에서는 isinstance() 검사로 어떤 타입이 버퍼 프로토콜을 구현하는지 확인할 수 있도록, C로 구현된 __instancecheck__를 가진 새로운 types.Buffer 타입을 추가하자고 제안했습니다. 이 방식은 전체 버퍼 프로토콜을 Python 코드에 노출하는 복잡성을 피하면서도, 타입 시스템이 버퍼 프로토콜을 검사할 수 있게 합니다.

그러나 types.Buffer는 구조적 타입이 아닌 명목적 타입이 되므로, 이 접근 방식은 타입 시스템의 나머지 부분과 잘 조합되지 않습니다. 예를 들어 “버퍼 프로토콜과 __len__을 모두 지원하는 객체”를 표현할 방법이 없게 됩니다. 현재 제안에서는 __buffer__가 다른 특수 메서드와 다르지 않으므로, 이를 다른 메서드와 결합한 Protocol을 정의할 수 있습니다.

더 일반적으로 말하면, Python의 다른 어떤 부분도 제안된 types.Buffer처럼 동작하지 않습니다. 언어의 나머지 부분에서는 C 수준 슬롯에 대개 대응하는 Python 수준 특수 메서드가 있으며, 현재 제안은 이러한 방식과 더 일관성이 있습니다.

bytearray를 bytes와 호환되도록 유지하기

memoryview가 항상 bytes와 호환되는 특수한 경우는 제거하되, 두 타입의 인터페이스가 매우 비슷하므로 bytearray에 대해서는 이를 유지하자는 제안이 있었습니다. 그러나 여러 표준 라이브러리 함수(예: re.compile, socket.getaddrinfo, 경로 형식의 인자를 받는 대부분의 함수)는 bytes는 받지만 bytearray는 받지 않습니다. 또한 대부분의 코드베이스에서 bytearray는 그다지 흔히 쓰이는 타입이 아닙니다. 저희는 사용자가 허용하는 타입을 명시적으로 적는 방식(또는 특정 메서드 집합만 필요하다면 PEP 544의 Protocol을 사용하는 방식)을 선호합니다. 제안의 이 부분은 typing-sig 메일링 리스트에서 구체적인 논의를 거쳤으며, 타이핑 커뮤니티에서 강한 반대는 없었습니다.

가변 버퍼와 불변 버퍼의 구분

버퍼 타입 사이에서 가장 자주 쓰이는 구분은 버퍼가 가변인지 여부입니다. 어떤 함수는 가변 버퍼(예: bytearray, 일부 memoryview 객체)만 받고, 다른 함수는 모든 버퍼를 받습니다.

이 PEP의 이전 버전에서는 버퍼 타입이 가변인지 판단하는 데 bf_releasebuffer 슬롯의 존재 여부를 사용하자고 제안했습니다. 이 규칙은 대부분의 표준 라이브러리 버퍼 타입에 들어맞지만, 가변성과 이 슬롯의 존재 여부 사이의 관계가 절대적인 것은 아닙니다. 예를 들어, numpy 배열은 가변이지만 이 슬롯이 없습니다.

현재의 버퍼 프로토콜은 버퍼 타입이 가변 버퍼를 나타내는지 불변 버퍼를 나타내는지를 신뢰성 있게 판단할 방법을 제공하지 않습니다. 따라서 이 PEP는 이러한 구분에 대한 타입 시스템 지원을 추가하지 않습니다. 향후 버퍼 프로토콜이 정적 인트로스펙션을 지원하도록 개선되면 이 문제를 다시 검토할 수 있습니다. 이러한 메커니즘에 대한 스케치가 있습니다.

감사의 글

많은 분이 이 PEP의 초안에 유용한 피드백을 주셨습니다. 특히 Petr Viktorin은 제가 버퍼 프로토콜의 미묘한 부분을 이해하는 데 큰 도움을 주셨습니다.