PEP 3154 – Pickle 프로토콜 버전 4
- Author:
- Antoine Pitrou <solipsis at pitrou.net>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 11-Aug-2011
- Python-Version:
- 3.4
- Post-History:
- 12-Aug-2011
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
pickle 모듈을 사용하여 직렬화한 데이터는 Python 버전 간에 이식 가능해야 합니다. 또한 최신 언어 기능뿐만 아니라 구현별 기능도 지원해야 합니다. 이러한 이유로 pickle 모듈은 여러 프로토콜을 알고 있으며(현재 0부터 3까지 번호가 지정되어 있음), 각 프로토콜은 서로 다른 Python 버전에 등장했습니다. 번호가 낮은 프로토콜 버전을 사용하면 이전 Python 버전과 데이터를 교환할 수 있는 반면, 번호가 높은 프로토콜을 사용하면 더 최신 기능에 액세스할 수 있고 때로는 리소스를 더 효율적으로 사용할 수 있습니다(직렬화 및 역직렬화에 필요한 CPU 시간과 데이터 전송에 필요한 디스크 크기 및 네트워크 대역폭 모두에서 그렇습니다).
근거
우연히 프로토콜 3이라고 명명된 현재의 최신 프로토콜은 Python 3.0과 함께 등장했으며, 언어에 새로 추가된 호환되지 않는 기능을 지원합니다(주로 기본 유니코드 문자열과 새로운 bytes 객체를 지원합니다). 당시에는 다른 방식으로 프로토콜을 개선할 기회를 활용하지 않았습니다.
이 PEP는 새로운 pickle 프로토콜 버전에서 여러 점진적 개선을 촉진하려는 시도입니다. 새로운 pickle 프로토콜의 도입은 드문 일이 되어야 하므로, 가능한 한 많은 개선 사항을 모으기 위해 PEP 절차를 사용합니다.
제안된 변경 사항
프레이밍
전통적으로 스트림에서 객체를 언피클링할 때(loads()가 아닌 load()를 호출할 때), 파일과 유사한 객체에 대해 작은 read() 호출을 많이 수행할 수 있으며, 이로 인해 성능에 잠재적으로 큰 영향을 줄 수 있습니다.
반면 프로토콜 4는 바이너리 프레이밍을 제공합니다. 따라서 pickle의 일반적인 구조는 다음과 같습니다.:
+------+------+
| 0x80 | 0x04 | protocol header (2 bytes)
+------+------+
| OP | FRAME opcode (1 byte)
+------+------+-----------+
| MM MM MM MM MM MM MM MM | frame size (8 bytes, little-endian)
+------+------------------+
| .... | first frame contents (M bytes)
+------+
| OP | FRAME opcode (1 byte)
+------+------+-----------+
| NN NN NN NN NN NN NN NN | frame size (8 bytes, little-endian)
+------+------------------+
| .... | second frame contents (N bytes)
+------+
etc.
구현을 단순하게 유지하기 위해 pickle opcode가 프레임 경계를 걸쳐 존재하는 것은 금지됩니다. 피클러는 이러한 pickle을 생성하지 않도록 주의하며, 언피클러는 이를 거부합니다. 또한 “마지막 프레임” 표시는 없습니다. 마지막 프레임은 단순히 STOP opcode로 끝나는 프레임입니다.
잘 작성된 C 구현은 프레이밍 계층에서 추가 메모리 복사본이 필요하지 않으므로, 전반적인 피클링 및 언피클링 효율성을 유지합니다.
Note
피클러가 pickle 스트림을 프레임으로 분할하는 방식은 구현 세부 사항입니다. 예를 들어 프레임의 크기가 ~64 KiB에 도달하는 즉시 프레임을 “닫는” 것은 성능과 pickle 크기 오버헤드 측면에서 모두 합리적인 선택입니다.
모든 opcode의 바이너리 인코딩
프로토콜 3에서도 여전히 사용되는 GLOBAL opcode는 pickle 프로토콜의 이른바 “텍스트” 모드를 사용하며, 이 모드에서는 pickle 스트림에서 줄바꿈을 찾습니다. 또한 이는 바이너리 프레이밍의 구현을 복잡하게 만듭니다.
프로토콜 4에서는 GLOBAL opcode의 사용을 금지하고, 스택에서 피연산자를 가져오는 새로운 opcode인 STACK_GLOBAL로 대체합니다.
더 많은 “조회 가능한” 객체 직렬화
기본적으로 pickle은 모듈 전역 함수와 클래스만 직렬화할 수 있습니다. 언바운드 메서드 [4]를 비롯한 다른 종류의 객체를 지원해 달라는 요청은 흔합니다. 실제로 바운드 메서드와 같은 일부 객체에 대한 서드파티 지원은 multiprocessing 모듈 [5]에 구현되어 있습니다.
이름으로 조회할 수 있는 객체의 수를 훨씬 늘릴 수 있도록 __qualname__ 속성을 PEP 3155에서 도입합니다. STACK_GLOBAL opcode가 점으로 구분된 이름을 허용하게 하면 표준 pickle 구현에서 그러한 모든 종류의 객체를 지원할 수 있습니다.
대형 객체를 위한 64비트 opcode
현재 프로토콜 버전은 다양한 내장 타입(str, bytes)의 객체 크기를 32비트 정수로 기록합니다. 이로 인해 대용량 데이터를 직렬화할 수 없습니다 [1]. 매우 큰 bytes 및 str 객체를 지원하려면 새로운 opcode가 필요합니다.
set 및 frozenset을 위한 네이티브 opcode
str, bytes, dict, list, tuple과 같은 많은 일반적인 내장 타입에는 직렬화 및 역직렬화 시 리소스 사용량을 개선하기 위한 전용 opcode가 있지만, set 및 frozenset에는 그러한 opcode가 없습니다. 이러한 opcode를 추가하면 당연히 개선될 것입니다. 또한 전용 set 지원을 통해 자기 참조 집합을 피클링할 수 없는 현재의 문제를 해결하는 데 도움이 될 수 있습니다 [2].
키워드 인자를 사용한 __new__ 호출
현재 __new__에서 키워드 전용 인자를 사용하도록 요구하는 클래스는 피클링할 수 없습니다(더 정확히는 언피클링할 수 없습니다) [3]. 새로운 특수 메서드(__getnewargs_ex__)와 새로운 opcode(NEWOBJ_EX)가 모두 필요합니다. __getnewargs_ex__ 메서드가 존재하는 경우, 두 항목으로 이루어진 튜플 (args, kwargs)를 반환해야 하며, 첫 번째 항목은 위치 인자의 튜플이고 두 번째 항목은 클래스의 __new__ 메서드에 전달할 키워드 인자의 딕셔너리여야 합니다.
더 나은 문자열 인코딩
현재 짧은 str 객체는 길이가 4바이트 정수로 인코딩되므로 비효율적입니다. 길이에 1바이트를 사용하는 전용 opcode를 사용하면 많은 피클의 크기를 줄일 수 있습니다.
더 작은 메모이제이션
PUT opcode는 모두 스택 최상단 항목을 메모 딕셔너리의 어느 항목에 메모이제이션할지 선택하기 위한 명시적 인덱스를 요구합니다. 그러나 실제로 이러한 숫자는 순차적으로 할당됩니다. 대신 새로운 opcode인 MEMOIZE는 현재 메모 딕셔너리의 크기와 같은 인덱스에 스택 최상단 항목을 저장합니다. PUT opcode는 모든 비원자적 데이터 형식에 대해 생성되므로, 이를 통해 피클을 더 짧게 만들 수 있습니다.
새로운 opcode 요약
다음은 제안된 구현의 상태를 반영합니다(대부분 Alexandre Vassalotti의 작업 덕분입니다).
FRAME: 새 프레임을 도입합니다(8바이트 프레임 크기와 프레임 내용이 뒤따릅니다).SHORT_BINUNICODE: 1바이트 크기 접두사를 사용하는 utf8 인코딩 str 객체를 스택에 푸시합니다(따라서 길이가 256바이트 미만입니다).BINUNICODE8: 8바이트 크기 접두사를 사용하는 utf8 인코딩 str 객체를 스택에 푸시합니다(2**32바이트보다 긴 문자열에 사용되며, 이러한 문자열은BINUNICODE를 사용해 직렬화할 수 없습니다).BINBYTES8: 8바이트 크기 접두사를 사용하는 bytes 객체를 스택에 푸시합니다(2**32바이트보다 긴 bytes 객체에 사용되며, 이러한 객체는BINBYTES를 사용해 직렬화할 수 없습니다).EMPTY_SET: 새로운 빈 set 객체를 스택에 푸시합니다.ADDITEMS: 스택 최상단의 항목들을 set에 추가합니다(EMPTY_SET과 함께 사용합니다).FROZENSET: 스택 최상단의 항목들로 frozenset 객체를 만들고 이를 스택에 푸시합니다.NEWOBJ_EX: 스택 최상단의 세 항목인cls,args및kwargs를 가져와cls.__new__(*args, **kwargs)를 호출한 결과를 스택에 푸시합니다.STACK_GLOBAL: 최상위 두 스택 항목인module_name과qualname을 가져와,module_name이라는 이름의 모듈에서 점으로 구분된qualname을 조회한 결과를 스택에 푸시합니다.MEMOIZE: 스택 최상위 객체를 현재 메모 딕셔너리 크기와 같은 인덱스로 메모 딕셔너리에 저장합니다.
대안
프리페칭
Serhiy Storchaka는 알려진 피클 청크를 명시적으로 선언하기 위해 프레이밍을 특수한 PREFETCH 오피코드(2바이트 또는 4바이트 인자를 사용함)로 대체하자고 제안했습니다. 대용량 데이터는 이러한 청크 외부에서 피클링할 수 있습니다. 단순한 언피클러는 PREFETCH 오피코드를 건너뛰면서도 피클을 올바르게 디코딩할 수 있어야 하지만, 적절한 오류 처리를 위해서는 PREFETCH 길이가 오피코드 경계에 맞는지 확인해야 합니다.
감사의 말
알파벳순:
참고 자료
Copyright
This document has been placed in the public domain.