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

Python 개선 제안 한국어 번역

PEP 3154 – 피클 프로토콜 버전 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

Table of Contents

번역·라이선스 안내

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

초록

pickle 모듈로 직렬화한 데이터는 Python 버전이 달라도 이식 가능해야 합니다. 또한 최신 언어 기능뿐만 아니라 구현별 기능도 지원해야 합니다. 이러한 이유로 pickle 모듈은 여러 프로토콜(현재 0부터 3까지 번호가 매겨져 있음)을 알고 있으며, 각 프로토콜은 서로 다른 Python 버전에서 등장했습니다. 번호가 낮은 프로토콜 버전을 사용하면 오래된 Python 버전과 데이터를 교환할 수 있고, 번호가 높은 프로토콜을 사용하면 더 새로운 기능을 이용할 수 있으며 때로는 자원을 더 효율적으로 사용할 수 있습니다(직렬화 및 역직렬화에 필요한 CPU 시간과 데이터 전송에 필요한 디스크 크기 / 네트워크 대역폭 모두에서 그렇습니다).

근거

공교롭게도 프로토콜 3이라는 이름이 붙은 현재 최신 프로토콜은 Python 3.0과 함께 등장했으며, 언어에 새로 추가된 비호환 기능(주로 기본 유니코드 문자열과 새로운 bytes 객체)을 지원합니다. 당시에는 이 기회를 활용해 프로토콜을 다른 방면으로 개선하지 않았습니다.

이 PEP는 새로운 피클 프로토콜 버전에서 여러 점진적 개선을 추진하려는 시도입니다. 새로운 피클 프로토콜의 도입은 드물게 일어나야 하는 일이므로, 가능한 한 많은 개선 사항을 모으기 위해 PEP 절차를 사용합니다.

제안하는 변경 사항

프레이밍

전통적으로 스트림에서 객체를 언피클링할 때(loads()가 아니라 load()를 호출하여), 파일류 객체에 대해 작은 read() 호출이 많이 발생할 수 있으며, 이는 성능에 잠재적으로 막대한 영향을 줄 수 있습니다.

반면 프로토콜 4는 바이너리 프레이밍을 제공합니다. 따라서 피클의 일반적인 구조는 다음과 같습니다:

+------+------+
| 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.

구현을 단순하게 유지하기 위해, 피클 옵코드가 프레임 경계에 걸치는 것은 금지됩니다. 피클러는 그러한 피클을 생성하지 않도록 주의하며, 언피클러는 그러한 피클을 거부합니다. 또한 “마지막 프레임” 표시는 없습니다. 마지막 프레임은 단순히 STOP 옵코드로 끝나는 프레임입니다.

잘 작성된 C 구현은 프레이밍 계층을 위한 추가 메모리 복사가 필요하지 않으므로, 전반적인 피클링 및 언피클링 효율을 유지합니다.

Note

피클러가 피클 스트림을 프레임으로 어떻게 분할할지 결정하는 방식은 구현 세부 사항입니다. 예를 들어, 프레임이 약 64 KiB에 도달하는 즉시 프레임을 “닫는” 것은 성능과 피클 크기 오버헤드 양쪽 모두에서 합리적인 선택입니다.

모든 옵코드에 대한 바이너리 인코딩

프로토콜 3에서 여전히 사용되는 GLOBAL 옵코드는 pickle 프로토콜의 이른바 “텍스트” 모드를 사용하며, 이 모드에서는 pickle 스트림에서 줄바꿈을 찾아야 합니다. 이는 또한 바이너리 프레이밍의 구현을 복잡하게 만듭니다.

프로토콜 4는 GLOBAL 옵코드의 사용을 금지하고, 이를 스택에서 피연산자를 가져오는 새로운 옵코드인 STACK_GLOBAL로 대체합니다.

더 많은 “조회 가능한” 객체의 직렬화

기본적으로 pickle은 모듈 전역 함수와 클래스만 직렬화할 수 있습니다. 바인딩되지 않은 메서드 [4] 같은 다른 종류의 객체를 지원해 달라는 요청은 흔합니다. 실제로 바인딩된 메서드 같은 일부 객체에 대한 서드파티 지원이 multiprocessing 모듈 [5]에 구현되어 있습니다.

PEP 3155의 __qualname__ 속성 덕분에 훨씬 더 많은 객체를 이름으로 조회할 수 있습니다. STACK_GLOBAL 옵코드가 점으로 구분된 이름을 받도록 하면 표준 pickle 구현이 그러한 종류의 객체를 모두 지원할 수 있습니다.

큰 객체를 위한 64비트 옵코드

현재 프로토콜 버전들은 여러 내장 타입(str, bytes)의 객체 크기를 32비트 정수로 내보냅니다. 이 때문에 큰 데이터를 직렬화할 수 없습니다 [1]. 매우 큰 bytes 및 str 객체를 지원하려면 새로운 옵코드가 필요합니다.

set과 frozenset을 위한 네이티브 옵코드

많은 일반적인 내장 타입(str, bytes, dict, list, 튜플 등)에는 직렬화와 역직렬화 시 자원 소비를 개선하기 위한 전용 옵코드가 있습니다. 그러나 set과 frozenset에는 그런 옵코드가 없습니다. 그러한 옵코드를 추가하는 것은 명백한 개선이 될 것입니다. 또한 전용 set 지원은 현재 자기 참조 set을 피클링할 수 없는 문제를 해결하는 데 도움이 될 수 있습니다 [2].

키워드 인자로 __new__ 호출하기

현재 __new__가 키워드 전용 인자의 사용을 요구하는 클래스는 피클할 수 없습니다(정확히는 언피클할 수 없습니다) [3]. 새로운 특수 메서드(__getnewargs_ex__)와 새로운 옵코드(NEWOBJ_EX)가 모두 필요합니다. __getnewargs_ex__ 메서드가 존재한다면 2-튜플 (args, kwargs)를 반환해야 하며, 첫 번째 항목은 클래스의 __new__ 메서드에 전달할 위치 인자의 튜플이고 두 번째 항목은 키워드 인자의 dict입니다.

더 나은 문자열 인코딩

현재 짧은 str 객체의 길이는 4바이트 정수로 인코딩되는데, 이는 낭비입니다. 1바이트 길이를 사용하는 전용 옵코드가 있으면 많은 피클의 크기가 작아질 것입니다.

더 작은 메모이제이션

PUT 옵코드는 모두 스택 최상단 항목을 메모 딕셔너리의 어느 항목에 메모이즈할지 선택하기 위해 명시적인 인덱스를 요구합니다. 그러나 실제로는 이 번호들이 순차적으로 할당됩니다. 새로운 옵코드인 MEMOIZE는 그 대신 스택 최상단 항목을 메모 딕셔너리의 현재 크기와 같은 인덱스에 저장합니다. PUT 옵코드는 원자적이지 않은 모든 데이터 타입에 대해 생성되므로, 이를 통해 피클을 더 짧게 만들 수 있습니다.

새로운 옵코드 요약

다음은 제안된 구현의 현재 상태를 반영합니다(대부분 Alexandre Vassalotti의 작업 덕분입니다):

  • FRAME: 새 프레임을 시작합니다(뒤이어 8바이트 프레임 크기와 프레임 내용이 옵니다).
  • SHORT_BINUNICODE: 1바이트 크기 접두사를 가진 utf8로 인코딩된 str 객체를 푸시합니다(따라서 길이가 256바이트 미만입니다).
  • BINUNICODE8: 8바이트 크기 접두사를 가진 utf8로 인코딩된 str 객체를 푸시합니다(2**32바이트보다 길어서 BINUNICODE로는 직렬화할 수 없는 문자열용입니다).
  • BINBYTES8: 8바이트 크기 접두사를 가진 bytes 객체를 푸시합니다(2**32바이트보다 길어서 BINBYTES로는 직렬화할 수 없는 bytes 객체용입니다).
  • 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는 알려진 피클 청크를 명시적으로 선언하기 위해 프레이밍을 (2바이트 또는 4바이트 인자를 갖는) 특수한 PREFETCH 옵코드로 대체하자고 제안했습니다. 큰 데이터는 이러한 청크 바깥에서 피클링될 수 있습니다. 단순한 언피클러는 PREFETCH 옵코드를 건너뛰고도 피클을 올바르게 디코딩할 수 있어야 하지만, 제대로 된 오류 처리를 하려면 PREFETCH 길이가 옵코드 경계에 맞아떨어지는지 확인해야 합니다.

감사의 글

알파벳순:

  • 두 번째 PEP 3154 구현을 시작한 Alexandre Vassalotti [6]
  • 프레이밍 제안을 논의한 Serhiy Storchaka [6]
  • Alexandre Vassalotti가 멘토를 맡은 Google Summer of Code 프로젝트로 첫 번째 PEP 3154 구현을 시작한 Stefan Mihaila [7].

참고 문헌