[Final] PEP 307 - Extensions to the pickle protocol

원문 링크: PEP 307 - Extensions to the pickle protocol

상태: Final 유형: Standards Track 작성일: 31-Jan-2003

PEP 307 – pickle 프로토콜 확장

개요

이 PEP는 Python 2.3에서 도입된 새로운 pickle 프로토콜을 설명하며, 기존 new-style 객체들의 pickle 처리 문제를 해결하고 pickle 크기를 최적화합니다. 주로 API 변경 사항에 중점을 두지만, 일부 바이트 스트림 형식 세부 사항도 다룹니다. pickle 바이트 스트림 형식은 pickletools.py 모듈에 공식적으로 문서화되어 있습니다.

동기

Python 2.2에서 new-style 객체를 pickle하는 것은 비효율적이었고, classic 클래스 인스턴스에 비해 pickle 크기가 과도하게 커지는 문제가 있었습니다. 예를 들어, 간단한 new-style 객체의 pickle 크기는 classic 객체보다 훨씬 컸습니다 (33바이트 vs 86바이트). 이러한 비대화는 new-style 객체가 pickle 가능하려면 __reduce__를 사용해야 했기 때문입니다. 새로운 프로토콜(프로토콜 2)을 도입함으로써, 이 문제는 해결되어 pickle 크기가 크게 줄어듭니다 (위 예시에서 35바이트).

프로토콜 버전

기존 pickle 프로토콜은 텍스트 모드와 바이너리 모드로 구분되었습니다. 이제 텍스트 모드는 프로토콜 0, 바이너리 모드는 프로토콜 1로 명명됩니다. 이 PEP에서 새로 소개하는 프로토콜은 프로토콜 2입니다. pickle 프로토콜의 전통에 따라 프로토콜 2는 프로토콜 1의 상위 집합입니다.

  • 프로토콜 식별: 프로토콜 2 pickle은 시작 부분에 해당 프로토콜 버전을 나타내는 새로운 opcode를 포함합니다. 이는 이전 Python 버전에서 프로토콜 2 pickleunpickle하려고 할 경우 즉시 “알 수 없는 opcode” 예외를 발생시킵니다.
  • protocol 인자: pickle 관련 함수, 메서드, 생성자에서 바이너리 모드를 나타내던 bin 인자는 protocol로 이름이 변경되었고, 이제 프로토콜 번호를 받습니다 (기본값은 0).
  • 최고 프로토콜 버전: -1protocol 인자로 전달하면 해당 구현이 지원하는 가장 높은 프로토콜 버전을 선택할 수 있습니다. picklecPickle 모듈은 HIGHEST_PROTOCOL 상수를 제공하여 모듈이 읽을 수 있는 가장 높은 프로토콜 번호를 확인할 수 있습니다.
  • bin 키워드 인자 경고: bin을 키워드 인자로 전달하는 방식은 PendingDeprecationWarning이 발생하며 Python 2.4에서는 DeprecationWarning으로 격상될 예정입니다.

보안 문제

이전 Python 버전에서는 unpickle__safe_for_unpickling__ 속성이 1로 설정되었거나 copy_reg.safe_constructors에 등록된 함수만 호출하도록 하는 “안전 검사” 기능이 있었습니다. 그러나 이 기능은 잘못된 보안 의식을 줄 수 있으며, 실제로 Python 2.2 pickle.py 모듈의 버그로 인해 우회될 수 있었습니다.

Python 2.3부터는 이러한 모든 안전 검사가 공식적으로 제거되었고, 다음과 같은 경고로 대체됩니다.

경고: 신뢰할 수 없거나 인증되지 않은 소스에서 받은 데이터를 unpickle하지 마십시오.

이 경고는 안전 검사가 존재하는 이전 Python 버전에도 동일하게 적용됩니다.

확장된 __reduce__ API

클래스가 pickle을 제어하기 위해 사용할 수 있는 여러 API가 있습니다. 가장 널리 사용되는 것은 __getstate____setstate__이지만, 가장 강력한 것은 __reduce__입니다.

__reduce__ 기능은 클래스에서 __reduce__ 메서드나 __reduce_ex__ 메서드를 구현하거나, copy_regreduce 함수를 선언하여 제공할 수 있습니다. 반환 값은 동일하게 해석됩니다.

중요: classic 클래스 인스턴스의 pickle__reduce__ 또는 __reduce_ex__ 메서드를 찾지 않으므로, classic 클래스는 __getinitargs__ 및/또는 __getstate__를 사용하여 pickle을 사용자 정의해야 합니다.

__reduce__는 문자열 또는 튜플을 반환해야 합니다.

  • 문자열 반환: 객체의 상태를 pickle하지 않고, 이름으로 참조되는 동등한 객체의 참조를 반환합니다. 반환되는 문자열은 객체의 로컬 이름이어야 합니다.
  • 튜플 반환: 길이가 2에서 5까지의 가변 크기 튜플입니다. 처음 두 항목(함수 및 인자)은 필수이며, 나머지 항목은 선택 사항입니다 (새로운 두 항목 포함).
    1. function (필수): 객체의 초기 버전을 생성하기 위해 호출되는 callable 객체입니다. 나중에 state를 추가하여 pickle된 상태를 완전히 재구성할 수 있습니다. 이 함수는 그 자체로 pickle 가능해야 합니다.
    2. arguments (필수): function에 대한 인자 목록을 제공하는 튜플입니다. None일 수도 있습니다 (Zope 2의 ExtensionClass를 위해 설계되었으나 deprecated).
    3. state (선택 사항, new in this PEP): 추가 상태입니다. None이 아니면 statepickle되고, unpickleobj.__setstate__(state)가 호출됩니다. __setstate__가 정의되지 않으면 기본 구현은 state가 인스턴스 변수 이름을 값에 매핑하는 사전이라고 가정합니다.
    4. listitems (선택 사항, new in this PEP): None이 아니면 연속적인 리스트 항목을 생성하는 iterator여야 합니다. 이 항목들은 pickle되고 obj.append(item) 또는 obj.extend(list_of_items)를 사용하여 객체에 추가됩니다. 주로 리스트 서브클래스에 사용됩니다.
    5. dictitems (선택 사항, new in this PEP): None이 아니면 (key, value) 형태의 튜플로 구성된 연속적인 사전 항목을 생성하는 iterator여야 합니다. 이 항목들은 pickle되고 obj[key] = value를 사용하여 객체에 저장됩니다. 주로 딕셔너리 서브클래스에 사용됩니다.

참고: Python 2.3부터 __reduce__None 값을 가진 state를 반환할 때 unpickle__setstate__는 호출되지 않습니다.

__reduce__ 구현이 Python 2.2와 2.3 모두에서 작동해야 하는 경우, pickle.format_version 변수를 확인하여 listitemsdictitems 기능 지원 여부를 결정할 수 있습니다. 이 값이 >= "2.0"이면 지원됩니다.

__reduce_ex__ API

__reduce__를 구현할 때 프로토콜 버전을 아는 것이 유용할 때가 있습니다. 이를 위해 __reduce__ 대신 __reduce_ex__ 메서드를 구현할 수 있습니다. __reduce_ex__는 존재할 경우 __reduce__보다 우선적으로 호출되며, 단일 정수 인자로 프로토콜 버전을 받습니다.

object 클래스는 __reduce____reduce_ex__를 모두 구현합니다. 하지만 서브클래스가 __reduce__만 오버라이드하고 __reduce_ex__는 오버라이드하지 않으면, __reduce_ex__ 구현이 이를 감지하고 __reduce__를 호출합니다.

__reduce__ 구현이 없는 경우 pickle 사용자 정의

특정 클래스에 __reduce__ 구현이 없는 경우, 세 가지 경우가 다르게 처리됩니다.

  1. classic 클래스 인스턴스, 모든 프로토콜:
    • __reduce__는 사용되지 않습니다. 대신 __getstate__, __setstate__, __getinitargs__ 메서드를 통해 pickle을 사용자 정의할 수 있습니다.
    • __getstate__: 객체 자체를 참조하지 않고 객체의 상태를 나타내는 pickle 가능한 값을 반환해야 합니다. 기본 구현은 self.__dict__를 반환합니다.
    • __setstate__: __getstate__ (또는 기본 구현)에 의해 반환된 값으로 호출됩니다. 기본 구현은 state가 인스턴스 변수 이름을 값에 매핑하는 사전이라고 가정합니다.
    • __getinitargs__: __setstate__를 호출하기 전에 새 객체를 생성해야 합니다. __getinitargs__ 메서드가 있을 경우, 이 메서드가 반환하는 튜플을 인자 목록으로 사용하여 클래스 생성자를 호출하여 인스턴스가 생성됩니다.
  2. new-style 클래스 인스턴스, 프로토콜 0 및 1:
    • Python 2.2와 동일하게 변경되지 않았습니다.
    • object에서 상속받은 기본 __reduce__ 구현이 사용됩니다. 이 기본 __reduce__copy_reg._reconstructorcallable로, (D, B, basestate)를 인자로 사용합니다.
    • __getstate__ 및/또는 __setstate__ 메서드를 정의하여 사용자 정의할 수 있습니다. __getstate__None과 같이 false로 간주되는 값을 반환하면 statepickle되지 않고 __setstate__는 호출되지 않습니다.
    • slots가 있는 new-style 클래스 인스턴스는 __getstate__ 메서드가 없으면 프로토콜 0 및 1로 pickle될 수 없습니다.
    • __getinitargs__는 무시됩니다.
  3. new-style 클래스 인스턴스, 프로토콜 2:
    • object에서 상속받은 기본 __reduce__ 구현은 무시됩니다. 대신 다른 기본 구현이 사용되어 프로토콜 0 또는 1보다 new-style 클래스 인스턴스를 더 효율적으로 pickle할 수 있습니다.
    • __getstate__, __setstate__, __getnewargs__ 세 가지 특수 메서드를 사용하여 사용자 정의합니다. (__getinitargs__는 다시 무시됩니다.)
    • __getstate__: classic 클래스와는 미묘한 차이가 있습니다. new-style 클래스의 __getstate__None을 반환하면 unpickle__setstate__가 전혀 호출되지 않습니다.
    • __setstate__: __getstate__가 반환하는 값 또는 위에서 설명한 기본 state로 호출됩니다.
    • __getnewargs__: __setstate__를 호출하기 전에 새 객체를 생성해야 합니다. 프로토콜 2에서는 C.__new__(C, *args)와 같이 새 객체가 생성되도록 하는 새로운 pickle opcode가 사용됩니다. args__getnewargs__ 메서드가 정의된 경우 반환되는 튜플입니다.

__newobj__ unpickling 함수

__reduce__가 반환하는 unpickling 함수(튜플의 첫 번째 항목)의 이름이 __newobj__인 경우, pickle 프로토콜 2에서는 특별한 처리가 이루어집니다. __newobj__라는 이름의 unpickling 함수는 cls.__new__(cls, *args)와 같은 의미를 갖는다고 가정합니다.

pickle 프로토콜 2는 이 이름의 unpickling 함수를 특별히 처리하여, __newobj__에 대한 참조를 pickle하지 않고 cls.__new__(cls, *args)를 반환하는 pickle opcode를 발행합니다. 이것이 프로토콜 2 pickleclassic pickle보다 훨씬 작은 주된 이유입니다.

확장 레지스트리 (Extension Registry)

프로토콜 2는 pickle 크기를 줄이기 위한 새로운 메커니즘을 지원합니다. 클래스 인스턴스가 pickle될 때, 클래스의 전체 이름(모듈 이름, 패키지 이름, 클래스 이름 포함)이 pickle에 포함됩니다. 특히 많은 작은 pickle을 생성하는 애플리케이션의 경우, 이는 각 pickle에서 반복되어야 하는 많은 오버헤드를 유발합니다.

확장 레지스트리는 가장 자주 사용되는 이름을 작은 정수로 나타낼 수 있게 하여 매우 효율적으로 pickle됩니다. 1-255 범위의 확장 코드는 opcode를 포함하여 2바이트만 필요하며, 256-65535 범위는 opcode를 포함하여 3바이트만 필요합니다.

확장 레지스트리는 확장 코드와 이름 간의 매핑으로 정의됩니다. unpickle될 때 확장 코드는 객체를 생성하지만, 이 객체는 모듈 이름과 클래스(또는 함수) 이름으로 이름을 해석하여 얻습니다.

확장 코드 범위는 다음과 같이 제안됩니다.

  • 0: 예약됨 (사용되지 않음)
  • 1 - 127: Python 표준 라이브러리용
  • 128 - 191: Zope용
  • 192 - 239: 서드파티용
  • 240 - 255: 사적 사용 (할당되지 않음)
  • 256 - MAX (2^31 - 1): 향후 할당용

확장 레지스트리 API

확장 레지스트리는 copy_reg 모듈의 private 전역 변수로 관리됩니다. 레지스트리를 조작하기 위해 다음 세 가지 함수가 정의됩니다.

  • add_extension(module, name, code): 확장 코드를 등록합니다. modulename은 문자열이어야 하며, code1부터 MAX까지의 정수여야 합니다.
  • remove_extension(module, name, code): 이전에 등록된 (module, name)code 간의 매핑을 제거합니다.
  • clear_extension_cache(): 확장 코드 구현이 자주 명명되는 객체를 로드하는 속도를 높이기 위해 캐시를 사용할 수 있습니다. 이 메서드를 호출하여 캐시를 비울 수 있습니다.

copy 모듈

전통적으로 copy 모듈은 copy()deepcopy() 작업을 사용자 정의하기 위한 pickle API의 확장된 서브셋을 지원했습니다. Python 2.3에서는 copy 모듈에 여러 변경 사항이 적용됩니다.

  • __reduce_ex__가 지원됩니다 (항상 프로토콜 버전 2로 호출됨).
  • __reduce__의 4개 및 5개 인자 반환 값이 지원됩니다.
  • __reduce__ 메서드를 찾기 전에 pickle과 마찬가지로 copy_reg.dispatch_table을 참조합니다.
  • object에서 __reduce__ 메서드를 상속받은 경우, pickle 프로토콜 2와 동일한 API(__getnewargs__, __getstate__, __setstate__)를 사용하여 더 나은 구현으로 (무조건) 대체됩니다.

이러한 변경으로 인해 Python 2.2에서 copy 가능했던 일부 new-style 클래스가 Python 2.3에서는 copy 불가능할 수 있습니다. 이는 __new__가 오버라이드되고 클래스 인자 외에 하나 이상의 필수 인자를 가질 때 발생합니다. 이를 해결하려면 적절한 인자 튜플을 반환하는 __getnewargs__ 메서드를 추가해야 합니다.

Python longs pickle

프로토콜 0과 1에서는 Python longs(정수)를 pickleunpickle하는 데 자릿수에 따라 2차 시간 복잡도가 소요되었습니다. 프로토콜 2에서는 새로운 opcode가 선형 시간 복잡도로 longs를 pickleunpickle할 수 있도록 지원합니다.

bools pickle

프로토콜 2는 TrueFalse를 직접 pickle하기 위한 새로운 opcode를 도입합니다. 프로토콜 0과 1에서는 bool이 정수로 pickle되었고, 이는 bool당 4바이트를 소비했습니다. 새로운 bool opcodebool당 1바이트를 소비합니다.

작은 튜플 pickle

프로토콜 2는 길이가 1, 2, 3인 튜플을 더 간결하게 pickle하기 위한 새로운 opcode를 도입합니다. 프로토콜 1에서는 빈 튜플을 더 간결하게 pickle하기 위한 opcode가 이전에 도입되었습니다.

대규모 리스트 및 딕셔너리 pickle

프로토콜 1은 대규모 리스트와 딕셔너리를 “한 덩어리”로 pickle하여 pickle 크기를 최소화하지만, unpickleunpickle되는 객체만큼 큰 임시 객체를 생성해야 했습니다. 프로토콜 2의 변경 사항 중 일부는 대규모 리스트와 딕셔너리를 최대 1000개의 요소로 구성된 조각으로 분할하여 unpickle 시 1000개의 요소를 담는 데 필요한 것보다 큰 임시 객체를 생성할 필요가 없도록 합니다.


This concludes the translation and summary of PEP 307.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments