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

Python 개선 제안 한국어 번역

PEP 296 – 바이트 객체 유형 추가

Author:
Scott Gilbert <xscottg at yahoo.com>
Status:
Withdrawn
Type:
Standards Track
Created:
12-Jul-2002
Python-Version:
2.3
Post-History:


Table of Contents

번역·라이선스 안내

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

공지

이 PEP는 작성자에 의해 철회되었습니다(PEP 358을 선호합니다).

개요

이 PEP는 ‘bytes’라는 새로운 표준 타입과 내장 생성자를 만들 것을 제안합니다. bytes 객체는 효율적으로 저장되는 바이트 배열이며, 여러 유사한 구현과 구별되는 몇 가지 추가 특성을 가집니다.

근거

현재 Python에는 이 제안의 bytes 객체와 유사한 기능을 구현하는 객체가 많이 있습니다. 예를 들어 표준 문자열, 버퍼, 배열 및 mmap 객체는 모두 일부 측면에서 bytes 객체와 매우 유사합니다. 또한 몇몇 중요한 서드파티 확장 기능은 유사한 요구를 충족하려고 비슷한 객체를 만들었습니다. 그러나 안타깝게도 이러한 객체는 각각 범위가 너무 좁고, 더 광범위한 문제 범주에 적용하는 데 필요한 핵심 기능이 빠져 있습니다.

사양

bytes 객체는 다음과 같은 중요한 특성을 가집니다.

  1. 표준 C 타입인 “unsigned char”를 통한 효율적인 내부 배열 저장입니다. 이를 통해 할당되는 메모리 양을 세밀하게 제어할 수 있습니다. 다음 항목에서 지정하는 정렬 제약 조건을 따르면 저수준 확장 기능이 필요에 따라 포인터를 다른 타입으로 변환하는 작업은 간단합니다.

    또한 객체가 바이트 배열로 구현되어 있으므로, 현재 문자열과 함께 작동하는 표준 라이브러리의 방대한 루틴 모음에 bytes 객체를 전달할 수 있습니다. 예를 들어 bytes 객체를 struct 모듈과 함께 사용하면 Python 스크립트만으로 array 모듈을 완전히 대체할 수 있습니다.

    네이티브 부호 없는 8비트 타입이 없는 비정상적인 플랫폼이 등장하는 경우, 이 객체는 Python 스크립트 수준에서 마치 8비트 부호 없는 값의 배열인 것처럼 자신을 표현하기 위해 최선을 다합니다. 많은 확장 기능이 이를 올바르게 처리할지는 의문이지만, 이러한 경우에도 Python 스크립트는 이식성을 가질 수 있습니다.

  2. 할당된 바이트 배열의 정렬은 플랫폼의 malloc 구현이 보장하는 범위에 따릅니다. 확장 기능에서 생성한 bytes 객체에는 확장 기능 작성자가 적절하다고 판단하는 임의의 정렬을 제공할 수 있습니다.

    이 정렬 제약 조건을 통해 bytes 객체를 모든 표준 C 타입의 저장 공간으로 사용할 수 있어야 하며, 여기에는 PyComplex 객체나 표준 C 타입인 다른 구조체도 포함됩니다. 필요에 따라 확장 기능에서 추가 정렬 제약 조건을 제공할 수 있습니다.

  3. bytes 객체는 문자열/배열 객체가 제공하는 시퀀스 연산의 일부를 구현하지만, 일부 경우에는 의미가 약간 다릅니다. 특히 슬라이스는 항상 새로운 bytes 객체를 반환하지만, 내부 메모리는 두 객체 간에 공유됩니다. 이러한 유형의 슬라이스 동작을 “뷰”를 생성한다고 부릅니다. 또한 bytes 객체에 대한 반복과 연결은 정의되지 않았으며 예외를 발생시킵니다.

    이러한 객체는 고성능 애플리케이션에서 사용될 가능성이 높으므로, 뷰 슬라이싱을 사용하기로 한 결정의 한 가지 동기는 bytes 객체 간 복사가 매우 효율적이어야 하며 임시 객체를 만들 필요가 없어야 한다는 것입니다. 다음 코드는 이를 보여 줍니다.:

    # create two 10 Meg bytes objects
    b1 = bytes(10000000)
    b2 = bytes(10000000)
    
    # copy from part of one to another with out creating a 1 Meg temporary
    b1[2000000:3000000] = b2[4000000:5000000]
    

    rvalue의 길이가 lvalue와 같지 않은 슬라이스 대입은 예외를 발생시킵니다. 그러나 슬라이스 대입은 겹치는 슬라이스에서도 올바르게 작동합니다(일반적으로 memmove를 사용하여 구현됩니다).

  4. 바이트 객체는 효율적인 직렬화를 위해 picklecPickle 모듈에서 네이티브 타입으로 인식됩니다. (사실상 이는 서드 파티 확장으로 구현할 수 없는 유일한 요구 사항입니다.)

    문자열로 데이터를 임시 복사하지 않고 바이트 유사 객체에 저장된 데이터를 직렬화해야 하는 요구를 해결하기 위한 부분적인 해결책이 과거에 구현되었습니다. array 객체의 tofile 및 fromfile 메서드가 이에 대한 좋은 예입니다. 바이트 객체도 이러한 메서드를 지원합니다. 그러나 피클링은 shelve 모듈이나 Python 객체의 RPC 구현과 같은 다른 상황에서도 유용하며, 효율적인 데이터 전송을 위해 최종 사용자가 서로 다른 두 가지 직렬화 메커니즘을 사용하도록 요구하는 것은 바람직하지 않습니다.

    XXX: 새 바이트 객체를 이전 버전의 Python에서 문자열 객체로 언피클할 수 있는 방식으로 피클링을 구현하도록 시도합니다.

    언피클링할 때 바이트 객체는 Python에서 할당한 메모리(malloc을 통해)로 생성됩니다. 따라서 확장 프로그램이 제공한 포인터가 가질 수 있었던 추가 속성(특수 정렬 또는 특수한 메모리 유형)은 손실됩니다.

    XXX: bytes 타입의 C 서브클래스가 언피클링될 메모리를 제공할 수 있도록 시도합니다. 예를 들어 PageAlignedBytes라는 파생 클래스는 페이지 정렬된 메모리로 언피클됩니다.

    int가 32비트인 모든 플랫폼(대부분의 플랫폼)에서는 현재 31비트로 표현할 수 있는 것보다 긴 문자열을 생성할 수 없습니다. 따라서 해당 작업이 불가능한 경우 문자열로 피클링하면 예외가 발생합니다.

    적어도 대용량 파일을 지원하는 플랫폼(많은 플랫폼)에서는 file.write() 메서드를 반복해서 호출하여 큰 바이트 객체를 파일로 피클링할 수 있어야 합니다.

  5. 바이트 타입은 PyBufferProcs 인터페이스를 지원하지만, 바이트 객체는 바이트 객체에 대한 참조가 유지되는 동안 포인터가 할당 해제되거나 재할당되지 않는다는 추가적인 보장을 제공합니다. 이는 바이트 객체가 생성된 후에는 크기를 변경할 수 없음을 의미하지만, PyBytes_Check(...) 테스트가 통과하면 별도의 스레드가 포인터가 가리키는 메모리를 조작하는 동안 전역 인터프리터 잠금(GIL)을 해제할 수 있습니다.

    이러한 바이트 객체의 특성 덕분에 비동기 파일 I/O나 PyBufferProcs에서 얻은 포인터가 전역 인터프리터 잠금과 독립적으로 사용되는 다중 프로세서 시스템과 같은 상황에서 바이트 객체를 사용할 수 있습니다.

    GIL이 해제된 후에도 포인터를 재할당하거나 해제할 수 없다는 사실을 알면 확장 프로그램 작성자는 진정한 동시성을 확보하고 포인터에 대한 장시간 실행 계산에 추가 프로세서를 활용할 수 있습니다.

  6. C/C++ 확장에서는 제공된 포인터와 소멸자 함수를 사용하여 참조 횟수가 0이 될 때 메모리를 해제하도록 바이트 객체를 생성할 수 있습니다.

    바이트 객체에 대한 슬라이싱의 특수 구현을 통해 여러 바이트 객체가 동일한 포인터/소멸자를 참조할 수 있습니다. 따라서 실제 포인터/소멸자에 대한 참조 횟수가 유지됩니다. 이 참조 횟수는 일반적으로 Python 객체와 연결되는 참조 횟수와는 별개입니다.

    XXX: 내부의 참조 횟수 관리 객체를 실제 Python 객체로 노출하는 것이 바람직할 수 있습니다. 적절한 사용 사례가 생기면 하위 호환성 손실 없이 나중에 이를 구현할 수 있어야 합니다.

  7. 바이트 객체를 읽기 전용으로 표시하는 것도 가능합니다. 이 경우 실제로는 변경할 수 없지만 바이트 객체의 다른 기능은 제공합니다.
  8. 바이트 객체는 Python LONG_LONG 타입을 사용하여 데이터의 길이를 추적합니다. 현재 PyBufferProcs 정의가 길이를 int의 크기로 제한하더라도 이 PEP에서는 해당 부분을 변경할 것을 제안하지 않습니다. 대신 확장 프로그램은 명시적으로 PyBytes_Check(...)를 호출하여 이 제한을 우회할 수 있으며, 호출이 성공하면 PyBytes_GetReadBuffer(...) 또는 PyBytes_GetWriteBuffer를 호출하여 객체의 포인터와 전체 길이를 LONG_LONG으로 가져올 수 있습니다.

    표준 PyBufferProcs 메커니즘을 사용하고 바이트 객체의 크기가 정수로 표현할 수 있는 범위를 초과하면 바이트 객체는 예외를 발생시킵니다.

    Python 스크립팅에서는 바이트 객체를 long 정수로 서브스크립트에 사용할 수 있으므로 32비트 int 제한을 피할 수 있습니다.

    len() 함수는 PyObject_Size()이고 이 함수 역시 int를 반환하므로 여전히 문제가 있습니다. 해결 방법으로 바이트 객체는 long을 반환하는 .length() 메서드를 제공합니다.

  9. 할당할 바이트 수와 함께 int/long을 bytes 생성자에 전달하면 Python 스크립팅 수준에서 bytes 객체를 생성할 수 있습니다. 예를 들어 다음과 같습니다.:
    b = bytes(100000) # alloc 100K bytes
    

    생성자는 다른 bytes 객체도 인자로 받을 수 있습니다. 이는 언피클링 구현과 읽기-쓰기 bytes 객체를 읽기 전용 객체로 변환하는 데 유용합니다. 선택적 두 번째 인자는 읽기 전용 bytes 객체의 생성을 지정하는 데 사용됩니다.

  10. C API에서는 다음 시그니처 중 하나를 사용하여 bytes 객체를 할당할 수 있습니다.:
    PyObject* PyBytes_FromLength(LONG_LONG len, int readonly);
    PyObject* PyBytes_FromPointer(void* ptr, LONG_LONG len, int readonly
             void (*dest)(void *ptr, void *user), void* user);
    

    PyBytes_FromPointer(...) 함수에서 dest 함수 포인터에 NULL이 전달되면 해당 함수는 호출되지 않습니다. 이는 정적으로 할당된 공간에서 bytes 객체를 생성하는 경우에만 사용해야 합니다.

    사용자 포인터는 다른 곳에서 클로저라고 불려 왔습니다. 이는 사용자가 어떤 목적에든 사용할 수 있는 포인터입니다. 정리할 때 소멸자 함수에 전달되며 여러 용도로 유용하게 사용할 수 있습니다. 사용자 포인터가 필요하지 않으면 대신 NULL을 전달해야 합니다.

  11. 모든 표준 Python 타입이 향하는 방향으로 보이므로 bytes 타입은 뉴 스타일 클래스가 됩니다.

기존 타입과의 대조

bytes 객체의 부재를 해결하는 가장 일반적인 방법은 그 자리에 단순히 문자열 객체를 사용하는 것이었습니다. 바이너리 파일, struct/array 모듈 및 여러 다른 사례가 이에 해당합니다. 이러한 사용이 일반적으로 텍스트 문자열과는 아무 관련이 없다는 스타일상의 문제를 차치하더라도, 문자열은 변경할 수 없으므로 이러한 경우 반환된 데이터를 직접 조작할 수 없다는 실제 문제가 있습니다. 또한 문자열 모듈의 수많은 최적화(해시 값 캐싱이나 포인터 인터닝 등) 때문에, 확장 모듈 작성자가 문자열 객체로 규칙을 어기려 한다면 매우 위험한 상황에 놓이게 됩니다.

버퍼 객체는 bytes 객체가 수행하려는 목적을 해결하기 위해 고안된 것으로 보이지만, 구현상의 몇 가지 단점 [1] 때문에 일반적인 많은 경우에 유용성이 떨어졌습니다. 버퍼 객체는 슬라이싱 동작에 대해 다른 선택을 했으며(슬라이싱 및 기타 연산에서 버퍼 대신 새 문자열을 반환합니다), bytes 객체가 제공하는 정렬에 관한 보장이나 GIL을 해제할 수 있다는 보장을 많이 제공하지 않습니다.

또한 버퍼 객체와 관련하여, 하위 호환성을 유지하면서 버퍼 객체를 bytes 객체로 단순히 대체하는 것은 불가능합니다. 버퍼 객체는 다른 객체가 제공하는 PyBufferProcs포인터를 가져와 자신의 포인터인 것처럼 제공하는 메커니즘을 제공합니다. 다른 객체의 동작이 bytes 객체와 동일한 엄격한 규칙 집합을 따른다고 보장할 수 없으므로, bytes 객체를 사용할 수 있는 곳에서 이를 사용할 수 없습니다.

array 모듈은 바이트 배열 생성을 지원하지만, 확장 모듈이 제공하는 메모리에 대한 포인터와 소멸자를 제공하기 위한 C API는 제공하지 않습니다. 이 때문에 공유 메모리나 DMA 전송과 같이 특수한 정렬 또는 잠금이 필요한 메모리로부터 객체를 생성하는 데 사용할 수 없습니다. 또한 array 객체는 현재 피클링을 지원하지 않습니다. 마지막으로 array 객체는 extend 메서드를 통해 콘텐츠를 확장할 수 있으므로, 이를 사용하는 동안 GIL이 유지되지 않으면 포인터가 변경될 수 있습니다.

array 객체에서 버퍼 객체를 생성할 때도 array 객체의 크기가 조정되면 유효하지 않은 포인터가 남는 동일한 문제가 발생합니다.

mmap 객체는 특정 영역의 요구를 충족하지만 더 광범위한 문제를 해결하려고 하지는 않습니다.

마지막으로 모든 서드 파티 확장은 표준 Python 타입의 임시 객체를 생성하지 않고는 피클링을 구현할 수 없습니다. 예를 들어 Numeric 커뮤니티에서는 큰 배열 데이터를 복제하기 위해 큰 바이너리 문자열을 생성하지 않고는 큰 배열을 피클링할 수 없다는 점이 불편합니다.

하위 호환성

저자가 알고 있는 하위 호환성 문제의 유일한 가능성은, 새로운 bytes 타입을 포함한 데이터를 unpickle하려 시도하는 이전 버전의 Python에서 발생합니다.

참조 구현

XXX: 실제 구현이 진행 중이지만, 이 PEP가 추가 검토를 거치면서 변경될 가능성이 있습니다.

다음 새 파일들이 Python 베이스라인에 추가될 예정입니다:

Include/bytesobject.h  # C interface
Objects/bytesobject.c  # C implementation
Lib/test/test_bytes.py # unit testing
Doc/lib/libbytes.tex   # documentation

다음 파일들도 수정될 예정입니다:

Include/Python.h       # adding bytesmodule.h include file
Python/bltinmodule.c   # adding the bytes type object
Modules/cPickle.c      # adding bytes to the standard types
Lib/pickle.py          # adding bytes to the standard types

다른 여러 모듈들도 정리되어 bytes 객체를 기반으로 구현될 수 있습니다. mmap 모듈이 가장 먼저 떠오르지만, 위에서 언급했듯이 array 모듈을 순수 Python 모듈로 재구현하는 것도 가능할 것입니다. 이 PEP가 실제로 소스 코드의 양을 어느 정도 줄일 수 있다는 점은 매력적이지만, 저자는 이것이 기존 애플리케이션을 깨뜨릴 불필요한 위험을 초래할 수 있으므로 현재로서는 피해야 한다고 생각합니다.

추가 참고사항/의견

  • Guido van Rossum은 mmap 객체로부터 bytes 객체를 생성할 수 있게 하는 것이 타당할지 궁금해했습니다. mmap 객체는 bytes 객체에 메모리를 제공하는 데 필요한 요구사항을 지원하는 것으로 보입니다. (크기가 조정되지 않으며, 포인터는 객체의 수명 동안 유효합니다.) 따라서 mmap 객체로부터 bytes 객체를 직접 생성할 수 있도록 mmap 모듈에 메서드를 추가할 수 있을 것입니다. 이를 구현하는 초기 시도로는, 위에서 설명한 PyBytes_FromPointer() 함수를 사용하고 mmap_object를 사용자 포인터로 전달하는 방법이 있을 것입니다. 소멸자 함수는 정리를 위해 mmap_object를 decref할 것입니다.
  • 토드 밀러는, void* 포인터에 더해 LONG_LONG 길이도 얻을 수 있도록 지원한다는 점을 제외하면 PyObject_AsReadBuffer()PyObject_AsWriteBuffer()와 유사한 PyObject_AsLargeReadBuffer()PyObject_AsLargeWriteBuffer라는 두 개의 새로운 함수가 있으면 유용할 수 있다고 지적합니다. 이 함수들은 확장 모듈 작성자가 (LONG_LONG 길이를 지원하는) bytes 객체와 (int 길이만 지원하는) 대부분의 다른 버퍼 유사 객체를 투명하게 다룰 수 있게 해줄 것입니다. 이 함수들은 특정한 PyByte_GetReadBuffer()PyBytes_GetWriteBuffer() 함수를 만드는 것을 대신하거나, 그에 더해 사용할 수 있을 것입니다.

    XXX: 저자는 이것이 다른 객체들이 결국 큰(64비트) 포인터를 지원할 수 있는 길을 열어준다는 점에서 매우 좋은 아이디어라고 생각하며, abstract.c와 abstract.h에만 영향을 미쳐야 한다고 봅니다. 이것을 위 내용에 추가해야 합니까?

  • PyBufferProcs 인터페이스의 세그먼트 수를 악용하는 것이 길이의 31비트 제한을 우회하는 좋은 편법이 아니라는 데 대체로 의견이 모아졌습니다. 이것이 무슨 뜻인지 모르신다면, 당신만 그런 것은 아닙니다. 파이썬 베이스라인의 대부분 코드와, 아마도 많은 서드파티 확장 모듈들도 세그먼트 수가 1이 아닐 때는 처리를 포기합니다.

참고 문헌