Python으로 데이터 직렬화1

2024. 1. 5. 19:00python/intermediate

적절한 데이터 직렬화 형식을 선택하세요
상태 유지 Python 객체 스냅샷 찍기
분산 처리를 위해 유선으로 실행 코드 보내기
HTTP 메시지 페이로드에 대한 인기 있는 데이터 형식을 채택
계층적, 테이블 형식 및 기타 데이터의 형태 직렬화
데이터 구조를 검증하고 발전시키기 위해 스키마 사용
데이터 직렬화 개요 보기
이름 직렬화는 컴퓨터 메모리에 있는 개체의 조밀한 그래프로 구성될 수 있는 데이터가 Bite의 선형 시퀀스 또는 시리즈가 ​​된다는 것을 의미합니다. 이러한 선형 표현은 전송하거나 저장하기에 완벽합니다. 원시 바이트는 다양한 프로그래밍 언어, 운영 체제 및 하드웨어 아키텍처에서 보편적으로 이해되므로 호환되지 않는 시스템 간에 데이터를 교환할 수 있습니다.

데이터 직렬화 형식 비교
코드 받기 : https://realpython.com/bonus/python-serialize-data-code/

받은 코드를 원하는 장소에 설치를 하십시요. 
!python3 -m venv venv/
!source venv/bin/activate
%cd python-serialize   # 설치한 장소 디렉토리
!python -m pip install -r requirements.txt
텍스트 대 바이너리# XML <feed xmlns="http://www.w3.org/2005/Atom"> <title>Real Python</title> <link href="https://realpython.com/atom.xml" rel="self"/> <link href="https://realpython.com/"/> <updated>2023-09-15T12:00:00+00:00</updated> <id>https://realpython.com/</id> <author> <name>Real Python</name> </author> <entry> <title>Bypassing the GIL for Parallel Processing in Python</title> <id>https://realpython.com/python-parallel-processing/</id> <link href="https://realpython.com/python-parallel-processing/"/> <updated>2023-09-13T14:00:00+00:00</updated> <summary>In this tutorial, you'll take a deep dive (...)</summary> <content type="html"> <div><p>Unlocking Python's true potential (...) </content> </entry> (...) </feed>
이제 위의 텍스트 기반 Atom 피드를 동등한 바이너리 피드와 비교해 보세요.
atom.bson 자료는 https://realpython.com/atom.xml에서 내려 받으세요.

!hexdump -C atom.bson | head
텍스트 데이터 형식의 또 다른 큰 장점은 텍스트가 균일하고 대부분 명확하게 해석된다는 것입니다. 거의 항상 널리 사용되는 UTF-8인 올바른 문자 인코딩을 알고 있는 한 요즘에는 상상할 수 있는 모든 하드웨어와 시스템 어디에서나 직렬화된 메시지를 읽을 수 있습니다. XML, JSON, YAML, CSV 등 널리 사용되는 직렬화 형식이 모두 텍스트를 기반으로 한다는 것은 당연합니다.

텍스트 및 이진 데이터 형식이 서로 비교되는 방식은 다음과 같습니다.

텍스트 바이너리
예 CSV, JSON, XML, YAML Avro, BSON, Parquet, 프로토콜 버퍼
가독성 사람과 기계가 읽을 수 있는 기계 판독 가능
처리 속도 데이터 세트가 클수록 속도가 느려짐 빠른
크기 낭비적인 장황함과 중복으로 인해 큼 콤팩트
이식성 높은 플랫폼 독립성을 보장하려면 특별한 주의가 필요할 수 있습니다.
구조 고정되거나 발전하며 종종 자체 문서화됨 일반적으로 고정되어 있으며 사전에 동의해야 합니다.
데이터 유형 대부분 텍스트이며 이진 데이터를 포함할 때 효율성이 떨어집니다. 텍스트 또는 바이너리 데이터
개인 정보 보호 및 보안 민감한 정보를 노출합니다 정보 추출이 더 어려워지지만 완전히 면역되지는 않습니다.
이 표를 사용하면 특정 요구 사항에 맞는 형식을 결정하기 전에 텍스트 형식과 이진 데이터 직렬화 형식 간의 주요 차이점을 이해할 수 있습니다.

스키마리스 vs 스키마 기반
텍스트인지 바이너리인지에 관계없이 많은 데이터 직렬화 형식에는 예상되는 형식에 대한 직렬화된 데이터의 구조의 공식적인 설명인 스키마 문서가 필요합니다. 동시에 일부 형식은 스키마가 없는 반면 다른 형식은 스키마를 사용하거나 사용하지 않고 작업할 수 있습니다.

스키마리스 스키마 기반
텍스트 JSON, XML, YAML JSON+JSON 스키마, XML+XML 스키마(XSD), XML+문서 유형 정의(DTD)
바이너리 비손, 피클 Avro, 프로토콜 버퍼
현재 형식에 따라 해당 스키마를 다르게 표현할 수 있습니다. 예를 들어 XML 문서에 XML 기반 XSD 스키마를 제공하는 것이 일반적이지만 바이너리 Avro 형식은 스키마에 JSON을 사용합니다. 반면에 프로토콜 버퍼는 자체 인터페이스 정의 언어(IDL)를 사용합니다.

앞서 본 Atom 피드 스키마는 다소 오래된 RELAX NG를 활용합니다. RELAX NG 형식은 XML 차세대 일반 언어를 나타냅니다. 보다 널리 사용되는 XML 스키마(XSD)와는 달리 XML 자체를 기반으로 하지 않습니다.

# RELAX NG # -*- rnc -*- # RELAX NG Compact Syntax Grammar for the # Atom Format Specification Version 11 namespace atom = "http://www.w3.org/2005/Atom" namespace xhtml = "http://www.w3.org/1999/xhtml" namespace s = "http://www.ascc.net/xml/schematron" namespace local = "" start = atomFeed | atomEntry # Common attributes atomCommonAttributes = attribute xml:base { atomUri }?, attribute xml:lang { atomLanguageTag }?, undefinedAttribute* # ...
스키마는 일반적으로 허용되는 요소 및 속성 집합, 배열, 관계 및 관련 제약 조건(예: 요소가 필요한지 또는 요소가 사용할 수 있는 값 범위)을 정의합니다. 데이터 직렬화 언어의 어휘와 문법이라고 생각하시면 됩니다.

이 개념은 테이블, 열 유형, 외래 키 등을 지정하는 관계형 데이터베이스 스키마와 유사합니다. 이는 데이터베이스를 처음부터 다시 생성하기 위한 청사진이며, 문서 형식으로도 사용할 수 있습니다. 런타임 시 데이터베이스 스키마는 데이터의 참조 무결성을 관리합니다. 마지막으로 Django객체 관계형 매핑(ORM)을 용이하게 하는 데 도움이 됩니다.

데이터 직렬화 형식으로 스키마를 사용하면 다음과 같은 이점이 있습니다.

자동화: 데이터의 공식 사양을 사용하면 다양한 프로그래밍 언어로 코드 스텁을 생성하여 각 언어의 기본 데이터 유형의 자동 직렬화 및 역직렬화를 처리할 수 있습니다. XML 형식을 사용하는 경우 이는 데이터 바인딩이라고도 합니다.
일관성: 스키마는 직렬화된 데이터에 대한 표준 구조를 적용하여 다양한 시스템에서 무결성과 일관성을 보장합니다.
문서: 스키마는 데이터 구조에 대한 명확한 정의를 제공하므로 정보가 어떻게 구성되어 있는지 빠르게 이해하는 데 도움이 됩니다.
효율성: 명시적인 필드 이름을 포함하는 대신 스키마를 참조하면 직렬화된 데이터의 크기가 줄어듭니다. 스키마는 미리 알려지거나 직렬화된 메시지에 포함될 수 있습니다.
상호 운용성: 서로 다른 애플리케이션이나 서비스 간에 스키마를 공유하면 서로 통신할 수 있어 통합이 용이해집니다.
검증: 스키마를 사용하면 직렬화된 데이터를 자동화된 방식으로 검증하여 잠재적인 오류를 조기에 포착할 수 있습니다.
빠른 프로토타입 제작이나 예측할 수 없는 레이아웃이 있는 구조화되지 않은 데이터로 작업할 때는 스키마 없는 데이터 직렬화 형식이 더 적합할 수 있습니다. 개념적으로 이는 여러 소스의 데이터를 받아들이고 처리할 수 있는 NoSQL 데이터베이스를 갖는 것과 같습니다. 새로운 유형의 요소나 알 수 없는 속성은 스키마 검증에 실패하는 대신 시스템을 손상시키지 않고 무시됩니다.

전체적으로 다음은 스키마 없는 데이터 직렬화 형식과 스키마 기반 데이터 직렬화 형식의 가장 중요한 장단점입니다.

스키마리스 스키마 기반
유연성 높음 구조화되지 않은 데이터를 처리하거나 모양을 쉽게 수정할 수 없습니다.
일관성 데이터 무결성이 문제가 될 수 있음 높음
크기 메타데이터의 반복적인 포함으로 인해 대용량 컴팩트(특히 스키마가 분리된 경우)
능률 빠른 저장, 느린 조회 균일한 구조로 인해 데이터 쿼리가 빠릅니다.
간단 구현이 간단함 더 많은 노력과 사전 계획이 필요함
범용 대 전문화
데이터 과학에서는 엄청난 양의 데이터를 처리해야 하는 경우가 많습니다. 성능을 최적화하고 스토리지 비용을 줄이려면 일반적으로 대규모 데이터세트 전용 바이너리 데이터 직렬화 형식을 선택하는 것이 좋습니다.

요즘에는 Parquet와 Feather가 인기를 끌고 있습니다. 데이터 과학 공간에서. 둘 다 서로 다른 라이브러리와 심지어 서로 다른 프로그래밍 언어 간의 데이터 공유를 허용하는 인메모리 사양인 Arrow와 호환됩니다. 오래되었지만 여전히 인기 있는 몇 가지 제품으로는 HDF5 및 NetCDF가 있습니다. 최신 버전인 Zarr는 분산 데이터 저장 및 계산에 대한 더 나은 지원을 제공합니다.

특수한 데이터 직렬화 형식은 다른 도메인에서도 나타났습니다. 몇 가지 예는 다음과 같습니다.

DICOM: 의료 이미지 저장 및 전송을 위한 바이너리 형식
GeoJSON: 지리 지형지물 직렬화를 위한 JSON의 특수 버전
GPX: GPS 좌표 교환을 위한 XML 기반 형식
MusicXML: 악보 저장을 위한 XML 기반 형식
OBJ: 3차원 모델을 저장하기 위한 텍스트 형식
이식성 vs Python 전용
Python은 다양한 목적으로 이진 데이터 직렬화 형식을 제공하는 표준 라이브러리의 다음 모듈과 함께 제공됩니다.

pickle: Python 객체 직렬화
marshal: 내부 객체 직렬화
shelve: Python 객체 지속성
dbm: Unix 데이터베이스에 대한 인터페이스
Python은 marshal 배후에서 사용하여 가져온 모듈의 바이트 코드가 포함된 특수 파일을 읽고 씁니다. . 처음으로 모듈을 가져올 때 인터프리터는 후속 가져오기 속도를 높이기 위해 컴파일된 지침과 함께 해당 .pyc 파일을 만듭니다. 다음은 모듈을 가져올 때 내부적으로 어떤 일이 일어나는지 대략적으로 보여주는 짧은 코드 조각입니다.

import shelve
with shelve.open("/tmp/cache.db") as shelf:
    shelf["last_updated"] = 1696846049.8469703
    shelf["user_sessions"] = {
        "jdoe@domain.com": {
            "user_id": 4185395169,
            "roles": {"admin", "editor"},
            "preferences": {
                "language": "en_US",
                "dark_theme": False
            }
        }
    }
shelve은 pickle 및 dbm 위에 구축된 편의 모듈이므로 후자를 사용하여 다음을 수행할 수 있습니다. 생성된 파일의 내용을 살펴보세요.

import dbm
with dbm.open("/tmp/cache.db") as db:
    for key in db.keys():
        print(f"{key} = {db[key]}")
Python 객체 직렬화
Python 객체를 pickle하세요.
import pickle

data = 255
with open("filename.pkl", mode="wb") as file:
    pickle.dump(data, file)

pickle.dumps(data)
먼저 pickle 모듈을 가져온 다음 dump() 또는 dumps()을 호출하여 임의의 Python을 변환해야 합니다.

for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
    print(f"v{protocol}:", pickle.dumps(data, protocol))
pickle.DEFAULT_PROTOCOL
pickle 된 데이터 역직렬화는 앞서 언급한 두 함수에 해당하는 모듈의 load() 또는 loads() 호출로 귀결됩니다.

import pickle

with open("filename.pkl", mode="rb") as file:
    pickle.load(file)
    
pickle.loads(b"\x80\x04K\xff.")
pickle 모듈은 앞으로 작업하게 될 대부분의 데이터 유형을 처리할 수 있습니다. 또한 그래프와 같은 참조 순환을 사용하여 객체를 처리할 수 있습니다.

cycle = {}
cycle["sibling"] = {"sibling": cycle}
pickle.loads(pickle.dumps(cycle))
recursive = []
recursive.append(recursive)
pickle.loads(pickle.dumps(recursive))
import sys
deeply_nested = []
for _ in range(sys.getrecursionlimit() // 4):
    deeply_nested = [deeply_nested]

pickle.loads(pickle.dumps(deeply_nested))
위 코드 스니펫의 변수 cycle 는 유일한 키-값 쌍이 다른 사전을 보유하는 사전이며, 이 사전은 다시 원래 사전에 대한 참조를 포함하여 순환을 형성합니다. 위의 recursive 목록에는 자신에 대한 참조인 요소가 하나만 있습니다. 마지막으로 deeply_nested 목록은 Python 최대 재귀 제한의 절반을 사용하여 마트료시카 인형처럼 목록을 서로 감싸줍니다.

그러나 객체를 pickle 하려고 할 때 오류를 일으키는 몇 가지 주목할만한 예외가 있습니다. 특히 다음 데이터 유형의 인스턴스는 설계상 pickling 이 불가능합니다.

Python의 재귀 한계에 접근하는 매우 깊게 중첩된 데이터 구조

람다 표현식

생성기 개체

Python 모듈

파일 객체

네트워크 소켓

데이터베이스 연결

스레드

스택 프레임

예를 들어, 람다 표현식, Python 모듈, 매우 깊게 중첩된 목록을 피클링하려고 할 때 발생하는 오류 종류는 다음과 같습니다.

pickle.dumps(lambda: None)
pickle.dumps(pickle)
very_deeply_nested = []
for _ in range(sys.getrecursionlimit()):
    very_deeply_nested = [very_deeply_nested]

pickle.dumps(very_deeply_nested)
from concurrent.futures import ProcessPoolExecutor

def worker(function, parameter):
    return function(parameter)

if __name__ == "__main__":
    with ProcessPoolExecutor() as pool:
        future = pool.submit(worker, lambda x: x**2, 10)
        future.result()
이 코드는 람다 식을 다른 시스템 프로세스로 보내려고 시도하기 때문에 피클링 오류가 발생합니다. concurrent.futures 모듈의 프로세스 풀이나 multiprocessing 모듈의 동등한 풀을 사용하면 실행 중인 작업자 간에 특정 유형의 데이터를 동시에 공유할 수 없습니다.

마찬가지로 Python에서 객체 복사는 pickle 모듈에 의존하는데, 이 모듈에도 동일한 결함이 있습니다.

def generator():
    yield

import copy
copy.copy(generator())
import dill
dill.dumps(42)
dill.loads(dill.dumps(lambda: None))
dill.dumps(generator())
기본 데이터 유형의 경우 dill는 pickle와 유사하게 작동하지만 좀 더 특이한 유형 중 일부를 직렬화할 수 있습니다 (예: 람다 표현식). 동시에 생성기 개체를 포함하여 모든 것을 직렬화할 수는 없습니다.

Pickling 프로세스 사용자 정의
피클된 클래스 인스턴스를 역직렬화하는 것은 초기화 메서드를 완전히 우회하므로 객체 복제와 유사합니다. 초기화 메소드에 객체에 대한 중요한 설정 코드가 포함되어 있는 경우 이는 이상적이지 않을 수 있습니다.

피클링 또는 피클링 프로세스를 사용자 정의해야 할 수 있는 기타 상황은 다음과 같습니다.

메타데이터 추가: 피클링 중에 객체의 일부가 아닌 추가 속성을 도입하여 타임스탬프와 같은 추가 정보로 직렬화된 바이트 스트림을 늘릴 수 있습니다.
민감한 정보 숨기기: 비밀번호 및 기타 비밀의 직렬화를 방지하려면 피클링에서 하나 이상의 속성을 제외할 수 있습니다.
상태 저장 객체 피클 해제: 객체에 데이터베이스 연결 또는 직접 직렬화할 수 없는 열린 파일과 같이 피클할 수 없는 상태가 있는 경우 다음을 수행해야 할 수 있습니다. 피클링 및 언피클링 중에 상태를 처리하기 위한 몇 가지 추가 논리를 포함하세요.
기본값 제공: 클래스 구조가 발전하면 이전 인스턴스에는 없었던 새로운 속성을 얻을 수 있습니다. 이러한 경우 오래된 객체를 언피클할 때 누락된 속성에 대한 기본값을 제공할 수 있습니다.
피클 크기 줄이기: 피클 해제 중에 일부 속성이 직렬화된 형식으로 많은 공간을 차지하는 경우 바이트 스트림에 유지하는 대신 일부 속성을 다시 계산할 수 있습니다.
피클링 프로세스에 연결하려면 클래스에 다음 두 가지 특수 메서드를 지정할 수 있습니다.

__getstate__()
__setstate__()
Python은 클래스의 인스턴스를 피클링하기 전에 첫 번째 인스턴스를 호출하며, 메소드가 속성 이름과 해당 값의 사전을 반환할 것으로 예상합니다. 반대로, 피클링을 해제하는 동안 Python은 두 번째 메서드를 호출하고 속성 사전을 전달하여 상태가 역직렬화된 후 개체를 적절하게 초기화할 수 있도록 합니다.

import time
from dataclasses import dataclass

@dataclass
class User:
    name: str
    password: str

    def __getstate__(self):
        state = self.__dict__.copy()
        state["timestamp"] = int(time.time())
        del state["password"]
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        with open("/dev/random", mode="rb") as file:
            self.password = file.read(8).decode("ascii", errors="ignore")
10행은 사용자 이름과 비밀번호로 구성된 객체의 내부 속성 사전 복사본을 만듭니다.
11행은 복사된 사전에 현재의 Unix 타임스탬프와 함께 timestamp라는 추가 속성을 삽입합니다.
12행은 새 사전에서 사용자의 비밀번호에 해당하는 키-값 쌍을 제거합니다.
13행은 개체의 수정된 상태가 포함된 새 사전을 pickle 모듈에 반환합니다.
16행은 pickle 모듈에서 제공하는 역직렬화된 상태를 사용하여 객체의 내부 사전을 채웁니다.
17행과 18행 Unix 계열 운영 체제의 /dev/random 파일에서 로드된 임의 바이트를 기반으로 새 비밀번호를 설정합니다.
JSON을 사용하여 객체 인코딩
모듈이 사용하는 바이너리 프로토콜과 달리pickle JSON은 사람이 읽을 수 있는 텍스트 직렬화 형식입니다. 엄청난 인기와 단순성 덕분에 다양한 프로그래밍 언어와 플랫폼에서 데이터 교환에 대해 보편적으로 받아들여지는 표준이 되었습니다.

다음 예에서는 Python 객체를 JSON 문자열로 덤프합니다.

import json

data = {
    "email": None,
    "name": "John Doe",
    "age": 42.5,
    "married": True,
    "children": ["Alice", "Bob"],
}

print(json.dumps(data, indent=4, sort_keys=True))
이 경우 키가 몇 가지 다른 데이터 유형에 매핑된 문자열인 Python 사전을 직렬화합니다. 결과적으로 얻는 것은 JSON의 문법 규칙에 따라 형식화된 Python 문자열입니다. 선택적으로 출력을 보기 좋게 인쇄하고 키를 알파벳 순으로정렬 요청할 수 있습니다. 더 큰 개체의 가독성을 향상시킵니다.

JSON 형식은 6가지 기본 데이터 유형만 지원합니다.

정렬: [1, 2, 3]
부울: true, false
null: null
번호: 42, 3.14
object: {"key1": "value", "key2": 42}
string: "Hello, World!"
json.dumps({"Saturday", "Sunday"})
위의 오류 메시지는 Python 세트가 JSON 형식을 사용하여 직렬화할 수 없음을 알려줍니다. 그러나 이러한 비표준 데이터 유형이나 사용자 정의 클래스를 처리하는 방법을 json 모듈에 가르칠 수 있습니다.

두 가지 방법이 있습니다. 덤프 및 로딩 함수에 콜백 함수를 제공하거나 json 모듈에서 인코더 및 디코더 클래스를 확장할 수 있습니다.

첫 번째 접근 방식을 사용하여 Python 세트를 JSON 문자열로 직렬화하려면 다음 콜백 함수를 정의하고 해당 참조를 default 매개변수를 통해 json.dump() 또는 json.dumps()에 전달할 수 있습니다.

def serialize_custom(value):
    if isinstance(value, set):
        return {
            "type": "set",
            "elements": list(value)
        }

data = {"weekend_days": {"Saturday", "Sunday"}}
json.dumps(data, default=serialize_custom)
Python은 이제 자체적으로 JSON으로 직렬화할 수 없는 모든 객체에 대해 serialize_custom()을 호출합니다. 해당 객체를 함수에 대한 인수로 전달하며, 알려진 데이터 유형 중 하나를 반환해야 합니다. 이 경우 함수는 목록으로 인코딩된 요소와 의도한 Python 데이터 유형을 나타내는 유형 필드가 포함된 사전으로 Python 세트를 나타냅니다.

def deserialize_custom(value):
    match value:
        case {"type": "set", "elements": elements}:
            return set(elements)
        case _:
            return value

json_string = """
    {
        "weekend_days": {
            "type": "set",
            "elements": ["Sunday", "Saturday"]
        }
    }
"""

json.loads(json_string, object_hook=deserialize_custom)
여기서 의 매개변수를 통해 deserialize_custom()에 대한 참조를 전달합니다. 사용자 정의 함수는 Python 사전을 인수로 사용하여 이를 원하는 객체로 변환할 수 있습니다. 구조적 패턴 일치를 사용하면 사전이 집합을 설명하는지 식별하고, 그렇다면 Python 집합 인스턴스를 반환합니다. 그렇지 않으면 사전을 있는 그대로 반환합니다.

이러한 일반 콜백 외에도 몇 가지 특수 콜백을 지정하여 JSON에서 정수 및 부동 소수점 숫자 리터럴을 역직렬화하는 방법을 재정의할 수 있습니다.

json.loads("42", parse_int=float)
from decimal import Decimal
json.loads("3.14", parse_float=Decimal)
json.loads("[NaN, Infinity, -Infinity]", parse_constant=str)
위의 코드 스니펫에서는 모든 정수를 Python 부동 소수점 수로 변환하고 모든 부동 소수점 숫자 리터럴을 십진수로 변환합니다.

cycle = {}
cycle["sibling"] = {"sibling": cycle}

json.dumps(cycle)
json.dumps(cycle, check_circular=False)
객체에 순환이 포함된 경우 순수 json 모듈은 이를 직렬화할 수 없습니다. 이미 직렬화한 객체를 추적하는 메커니즘을 구현하고 역직렬화 중에 메모리에 원본 객체 그래프를 재구성해야 합니다.

실행 코드 직렬화
피클 가져오기 가능 기호
# plus.py

def create_plus(x):
    def plus(y):
        return x + y

    return plus

plus_one = create_plus(1)
plus_two = lambda x: x + 2
import pickle
import plus
pickle.dumps(plus)
pickle.dumps(plus.create_plus)
결과 바이트 스트림은 상당히 짧으며 바깥쪽 모듈 이름(plus)을 포함하여 함수의 정규화된 이름을 포함합니다. 이제 이 바이트 시퀀스를 디스크에 저장하거나 네트워크를 통해 다른 Python 인터프리터로 전송할 수 있습니다. 그러나 수신측이 해당 함수나 클래스 정의에 액세스할 수 없는 경우에도 직렬화된 코드를 해제할 수 없습니다.

import importlib
import inspect

def get_module_source(module_name):
    module = importlib.import_module(module_name)
    return inspect.getsource(module)

source_code = get_module_source("plus")
print(source_code)
exec(source_code)
plus_two(3)
이 트릭은 코드 직렬화를 방지하지만 그 자체로 문제가 있습니다. 모듈의 나머지 부분을 공개하지 않고 특정 기능이나 클래스만 공유하고 싶다면 어떻게 해야 할까요? 아마도 모듈을 실행하면 원치 않는 부작용이 발생할 수 있습니다. 컴파일된 C 확장 모듈이기 때문에 고급 Python 소스 코드에 액세스하지 못할 수도 있습니다.

참고: 대신 dill를 사용하면 소스 코드 범위를 다음으로 편리하게 좁힐 수 있습니다.

import dill.source
from plus import plus_two
dill.source.getsource(plus_two)
람다 표현식으로 정의한 plus_two() 호출 가능 항목의 소스 코드만 가져옵니다. 그럼에도 불구하고 dill는 코드를 직렬화하는 더 나은 방법을 제공합니다.

코드 객체 직렬화
import pickle
import dill
import plus

pickle.dumps(plus.create_plus)
dill.dumps(plus.create_plus)
# shell python -i plus.py
create_plus()은 이제 전역 네임스페이스의 일부이므로 dill는 함수를 올바르게 직렬화할 수 있습니다. 마침내 모듈 없이 역직렬화할 수 있는 직렬화된 코드 객체를 포함하는 훨씬 더 긴 바이트 시퀀스를 출력합니다.

출처 : https://realpython.com/python-serialize-data/

  • 적절한 데이터 직렬화 형식을 선택하세요
  • 상태 유지 Python 객체 스냅샷 찍기
  • 분산 처리를 위해 유선으로 실행 코드 보내기
  • HTTP 메시지 페이로드에 대한 인기 있는 데이터 형식을 채택
  • 계층적, 테이블 형식 및 기타 데이터의 형태 직렬화
  • 데이터 구조를 검증하고 발전시키기 위해 스키마 사용

데이터 직렬화 개요 보기

이름 직렬화는 컴퓨터 메모리에 있는 개체의 조밀한 그래프로 구성될 수 있는 데이터가 Bite의 선형 시퀀스 또는 시리즈가 ​​된다는 것을 의미합니다. 이러한 선형 표현은 전송하거나 저장하기에 완벽합니다. 원시 바이트는 다양한 프로그래밍 언어, 운영 체제 및 하드웨어 아키텍처에서 보편적으로 이해되므로 호환되지 않는 시스템 간에 데이터를 교환할 수 있습니다.

데이터 직렬화 형식 비교

In [ ]:
받은 코드를 원하는 장소에 설치를 하십시요. 
In [ ]:
!python3 -m venv venv/
!source venv/bin/activate
%cd python-serialize   # 설치한 장소 디렉토리
In [ ]:
!python -m pip install -r requirements.txt

텍스트 대 바이너리

# XML 2023-09-15T12:00:00+00:00 https://realpython.com/ Real Pythonhttps://realpython.com/python-parallel-processing/ 2023-09-13T14:00:00+00:00In this tutorial, you'll take a deep dive (...)

Unlocking Python's true potential (...) (...)

이제 위의 텍스트 기반 Atom 피드를 동등한 바이너리 피드와 비교해 보세요.
atom.bson 자료는 https://realpython.com/atom.xml에서 내려 받으세요.

In [ ]:
!hexdump -C atom.bson | head

텍스트 데이터 형식의 또 다른 큰 장점은 텍스트가 균일하고 대부분 명확하게 해석된다는 것입니다. 거의 항상 널리 사용되는 UTF-8인 올바른 문자 인코딩을 알고 있는 한 요즘에는 상상할 수 있는 모든 하드웨어와 시스템 어디에서나 직렬화된 메시지를 읽을 수 있습니다. XML, JSON, YAML, CSV 등 널리 사용되는 직렬화 형식이 모두 텍스트를 기반으로 한다는 것은 당연합니다.

텍스트 및 이진 데이터 형식이 서로 비교되는 방식은 다음과 같습니다.

텍스트바이너리
CSV, JSON, XML, YAML Avro, BSON, Parquet, 프로토콜 버퍼
가독성 사람과 기계가 읽을 수 있는 기계 판독 가능
처리 속도 데이터 세트가 클수록 속도가 느려짐 빠른
크기 낭비적인 장황함과 중복으로 인해 큼 콤팩트
이식성 높은 플랫폼 독립성을 보장하려면 특별한 주의가 필요할 수 있습니다.
구조 고정되거나 발전하며 종종 자체 문서화됨 일반적으로 고정되어 있으며 사전에 동의해야 합니다.
데이터 유형 대부분 텍스트이며 이진 데이터를 포함할 때 효율성이 떨어집니다. 텍스트 또는 바이너리 데이터
개인 정보 보호 및 보안 민감한 정보를 노출합니다 정보 추출이 더 어려워지지만 완전히 면역되지는 않습니다.

이 표를 사용하면 특정 요구 사항에 맞는 형식을 결정하기 전에 텍스트 형식과 이진 데이터 직렬화 형식 간의 주요 차이점을 이해할 수 있습니다.

스키마리스 vs 스키마 기반

텍스트인지 바이너리인지에 관계없이 많은 데이터 직렬화 형식에는 예상되는 형식에 대한 직렬화된 데이터의 구조의 공식적인 설명인 스키마 문서가 필요합니다. 동시에 일부 형식은 스키마가 없는 반면 다른 형식은 스키마를 사용하거나 사용하지 않고 작업할 수 있습니다.

스키마리스스키마 기반
텍스트 JSON, XML, YAML JSON+JSON 스키마, XML+XML 스키마(XSD), XML+문서 유형 정의(DTD)
바이너리 비손, 피클 Avro, 프로토콜 버퍼

현재 형식에 따라 해당 스키마를 다르게 표현할 수 있습니다. 예를 들어 XML 문서에 XML 기반 XSD 스키마를 제공하는 것이 일반적이지만 바이너리 Avro 형식은 스키마에 JSON을 사용합니다. 반면에 프로토콜 버퍼는 자체 인터페이스 정의 언어(IDL)를 사용합니다.

앞서 본 Atom 피드 스키마는 다소 오래된 RELAX NG를 활용합니다. RELAX NG 형식은 XML 차세대 일반 언어를 나타냅니다. 보다 널리 사용되는 XML 스키마(XSD)와는 달리 XML 자체를 기반으로 하지 않습니다.

# RELAX NG # -*- rnc -*- # RELAX NG Compact Syntax Grammar for the # Atom Format Specification Version 11 namespace atom = "http://www.w3.org/2005/Atom" namespace xhtml = "http://www.w3.org/1999/xhtml" namespace s = "http://www.ascc.net/xml/schematron" namespace local = "" start = atomFeed | atomEntry # Common attributes atomCommonAttributes = attribute xml:base { atomUri }?, attribute xml:lang { atomLanguageTag }?, undefinedAttribute* # ...

스키마는 일반적으로 허용되는 요소 및 속성 집합, 배열, 관계 및 관련 제약 조건(예: 요소가 필요한지 또는 요소가 사용할 수 있는 값 범위)을 정의합니다. 데이터 직렬화 언어의 어휘와 문법이라고 생각하시면 됩니다.

이 개념은 테이블, 열 유형, 외래 키 등을 지정하는 관계형 데이터베이스 스키마와 유사합니다. 이는 데이터베이스를 처음부터 다시 생성하기 위한 청사진이며, 문서 형식으로도 사용할 수 있습니다. 런타임 시 데이터베이스 스키마는 데이터의 참조 무결성을 관리합니다. 마지막으로 Django객체 관계형 매핑(ORM)을 용이하게 하는 데 도움이 됩니다.

데이터 직렬화 형식으로 스키마를 사용하면 다음과 같은 이점이 있습니다.

  • 자동화: 데이터의 공식 사양을 사용하면 다양한 프로그래밍 언어로 코드 스텁을 생성하여 각 언어의 기본 데이터 유형의 자동 직렬화 및 역직렬화를 처리할 수 있습니다. XML 형식을 사용하는 경우 이는 데이터 바인딩이라고도 합니다.
  • 일관성: 스키마는 직렬화된 데이터에 대한 표준 구조를 적용하여 다양한 시스템에서 무결성과 일관성을 보장합니다.
  • 문서: 스키마는 데이터 구조에 대한 명확한 정의를 제공하므로 정보가 어떻게 구성되어 있는지 빠르게 이해하는 데 도움이 됩니다.
  • 효율성: 명시적인 필드 이름을 포함하는 대신 스키마를 참조하면 직렬화된 데이터의 크기가 줄어듭니다. 스키마는 미리 알려지거나 직렬화된 메시지에 포함될 수 있습니다.
  • 상호 운용성: 서로 다른 애플리케이션이나 서비스 간에 스키마를 공유하면 서로 통신할 수 있어 통합이 용이해집니다.
  • 검증: 스키마를 사용하면 직렬화된 데이터를 자동화된 방식으로 검증하여 잠재적인 오류를 조기에 포착할 수 있습니다.

빠른 프로토타입 제작이나 예측할 수 없는 레이아웃이 있는 구조화되지 않은 데이터로 작업할 때는 스키마 없는 데이터 직렬화 형식이 더 적합할 수 있습니다. 개념적으로 이는 여러 소스의 데이터를 받아들이고 처리할 수 있는 NoSQL 데이터베이스를 갖는 것과 같습니다. 새로운 유형의 요소나 알 수 없는 속성은 스키마 검증에 실패하는 대신 시스템을 손상시키지 않고 무시됩니다.

전체적으로 다음은 스키마 없는 데이터 직렬화 형식과 스키마 기반 데이터 직렬화 형식의 가장 중요한 장단점입니다.

스키마리스스키마 기반
유연성 높음 구조화되지 않은 데이터를 처리하거나 모양을 쉽게 수정할 수 없습니다.
일관성 데이터 무결성이 문제가 될 수 있음 높음
크기 메타데이터의 반복적인 포함으로 인해 대용량 컴팩트(특히 스키마가 분리된 경우)
능률 빠른 저장, 느린 조회 균일한 구조로 인해 데이터 쿼리가 빠릅니다.
간단 구현이 간단함 더 많은 노력과 사전 계획이 필요함

범용 대 전문화

데이터 과학에서는 엄청난 양의 데이터를 처리해야 하는 경우가 많습니다. 성능을 최적화하고 스토리지 비용을 줄이려면 일반적으로 대규모 데이터세트 전용 바이너리 데이터 직렬화 형식을 선택하는 것이 좋습니다.

요즘에는 Parquet와 Feather가 인기를 끌고 있습니다. 데이터 과학 공간에서. 둘 다 서로 다른 라이브러리와 심지어 서로 다른 프로그래밍 언어 간의 데이터 공유를 허용하는 인메모리 사양인 Arrow와 호환됩니다. 오래되었지만 여전히 인기 있는 몇 가지 제품으로는 HDF5 및 NetCDF가 있습니다. 최신 버전인 Zarr는 분산 데이터 저장 및 계산에 대한 더 나은 지원을 제공합니다.

특수한 데이터 직렬화 형식은 다른 도메인에서도 나타났습니다. 몇 가지 예는 다음과 같습니다.

  • DICOM: 의료 이미지 저장 및 전송을 위한 바이너리 형식
  • GeoJSON: 지리 지형지물 직렬화를 위한 JSON의 특수 버전
  • GPX: GPS 좌표 교환을 위한 XML 기반 형식
  • MusicXML: 악보 저장을 위한 XML 기반 형식
  • OBJ: 3차원 모델을 저장하기 위한 텍스트 형식

이식성 vs Python 전용

Python은 다양한 목적으로 이진 데이터 직렬화 형식을 제공하는 표준 라이브러리의 다음 모듈과 함께 제공됩니다.

  • pickle: Python 객체 직렬화
  • marshal: 내부 객체 직렬화
  • shelve: Python 객체 지속성
  • dbm: Unix 데이터베이스에 대한 인터페이스

Python은 marshal 배후에서 사용하여 가져온 모듈의 바이트 코드가 포함된 특수 파일을 읽고 씁니다. . 처음으로 모듈을 가져올 때 인터프리터는 후속 가져오기 속도를 높이기 위해 컴파일된 지침과 함께 해당 .pyc 파일을 만듭니다. 다음은 모듈을 가져올 때 내부적으로 어떤 일이 일어나는지 대략적으로 보여주는 짧은 코드 조각입니다.

In [ ]:
import shelve
with shelve.open("/tmp/cache.db") as shelf:
    shelf["last_updated"] = 1696846049.8469703
    shelf["user_sessions"] = {
        "jdoe@domain.com": {
            "user_id": 4185395169,
            "roles": {"admin", "editor"},
            "preferences": {
                "language": "en_US",
                "dark_theme": False
            }
        }
    }

shelve은 pickle 및 dbm 위에 구축된 편의 모듈이므로 후자를 사용하여 다음을 수행할 수 있습니다. 생성된 파일의 내용을 살펴보세요.

In [ ]:
import dbm
with dbm.open("/tmp/cache.db") as db:
    for key in db.keys():
        print(f"{key} = {db[key]}")

Python 객체 직렬화

Python 객체를 pickle하세요.

In [ ]:
import pickle

data = 255
with open("filename.pkl", mode="wb") as file:
    pickle.dump(data, file)

pickle.dumps(data)

먼저 pickle 모듈을 가져온 다음 dump() 또는 dumps()을 호출하여 임의의 Python을 변환해야 합니다.

In [ ]:
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
    print(f"v{protocol}:", pickle.dumps(data, protocol))
In [ ]:
pickle.DEFAULT_PROTOCOL

pickle 된 데이터 역직렬화는 앞서 언급한 두 함수에 해당하는 모듈의 load() 또는 loads() 호출로 귀결됩니다.

In [ ]:
import pickle

with open("filename.pkl", mode="rb") as file:
    pickle.load(file)
    
In [ ]:
pickle.loads(b"\x80\x04K\xff.")

pickle 모듈은 앞으로 작업하게 될 대부분의 데이터 유형을 처리할 수 있습니다. 또한 그래프와 같은 참조 순환을 사용하여 객체를 처리할 수 있습니다.

In [ ]:
cycle = {}
cycle["sibling"] = {"sibling": cycle}
pickle.loads(pickle.dumps(cycle))
In [ ]:
recursive = []
recursive.append(recursive)
pickle.loads(pickle.dumps(recursive))
In [ ]:
import sys
deeply_nested = []
for _ in range(sys.getrecursionlimit() // 4):
    deeply_nested = [deeply_nested]

pickle.loads(pickle.dumps(deeply_nested))

위 코드 스니펫의 변수 cycle 는 유일한 키-값 쌍이 다른 사전을 보유하는 사전이며, 이 사전은 다시 원래 사전에 대한 참조를 포함하여 순환을 형성합니다. 위의 recursive 목록에는 자신에 대한 참조인 요소가 하나만 있습니다. 마지막으로 deeply_nested 목록은 Python 최대 재귀 제한의 절반을 사용하여 마트료시카 인형처럼 목록을 서로 감싸줍니다.

그러나 객체를 pickle 하려고 할 때 오류를 일으키는 몇 가지 주목할만한 예외가 있습니다. 특히 다음 데이터 유형의 인스턴스는 설계상 pickling 이 불가능합니다.

  • Python의 재귀 한계에 접근하는 매우 깊게 중첩된 데이터 구조
  • 람다 표현식
  • 생성기 개체
  • Python 모듈
  • 파일 객체
  • 네트워크 소켓
  • 데이터베이스 연결
  • 스레드
  • 스택 프레임
  • 예를 들어, 람다 표현식, Python 모듈, 매우 깊게 중첩된 목록을 피클링하려고 할 때 발생하는 오류 종류는 다음과 같습니다.
In [ ]:
pickle.dumps(lambda: None)
In [ ]:
pickle.dumps(pickle)
In [ ]:
very_deeply_nested = []
for _ in range(sys.getrecursionlimit()):
    very_deeply_nested = [very_deeply_nested]

pickle.dumps(very_deeply_nested)
In [ ]:
from concurrent.futures import ProcessPoolExecutor

def worker(function, parameter):
    return function(parameter)

if __name__ == "__main__":
    with ProcessPoolExecutor() as pool:
        future = pool.submit(worker, lambda x: x**2, 10)
        future.result()

이 코드는 람다 식을 다른 시스템 프로세스로 보내려고 시도하기 때문에 피클링 오류가 발생합니다. concurrent.futures 모듈의 프로세스 풀이나 multiprocessing 모듈의 동등한 풀을 사용하면 실행 중인 작업자 간에 특정 유형의 데이터를 동시에 공유할 수 없습니다.

마찬가지로 Python에서 객체 복사는 pickle 모듈에 의존하는데, 이 모듈에도 동일한 결함이 있습니다.

In [ ]:
def generator():
    yield

import copy
copy.copy(generator())
In [ ]:
import dill
dill.dumps(42)
In [ ]:
dill.loads(dill.dumps(lambda: None))
In [ ]:
dill.dumps(generator())

기본 데이터 유형의 경우 dill는 pickle와 유사하게 작동하지만 좀 더 특이한 유형 중 일부를 직렬화할 수 있습니다 (예: 람다 표현식). 동시에 생성기 개체를 포함하여 모든 것을 직렬화할 수는 없습니다.

Pickling 프로세스 사용자 정의

피클된 클래스 인스턴스를 역직렬화하는 것은 초기화 메서드를 완전히 우회하므로 객체 복제와 유사합니다. 초기화 메소드에 객체에 대한 중요한 설정 코드가 포함되어 있는 경우 이는 이상적이지 않을 수 있습니다.

피클링 또는 피클링 프로세스를 사용자 정의해야 할 수 있는 기타 상황은 다음과 같습니다.

  • 메타데이터 추가: 피클링 중에 객체의 일부가 아닌 추가 속성을 도입하여 타임스탬프와 같은 추가 정보로 직렬화된 바이트 스트림을 늘릴 수 있습니다.
  • 민감한 정보 숨기기: 비밀번호 및 기타 비밀의 직렬화를 방지하려면 피클링에서 하나 이상의 속성을 제외할 수 있습니다.
  • 상태 저장 객체 피클 해제: 객체에 데이터베이스 연결 또는 직접 직렬화할 수 없는 열린 파일과 같이 피클할 수 없는 상태가 있는 경우 다음을 수행해야 할 수 있습니다. 피클링 및 언피클링 중에 상태를 처리하기 위한 몇 가지 추가 논리를 포함하세요.
  • 기본값 제공: 클래스 구조가 발전하면 이전 인스턴스에는 없었던 새로운 속성을 얻을 수 있습니다. 이러한 경우 오래된 객체를 언피클할 때 누락된 속성에 대한 기본값을 제공할 수 있습니다.
  • 피클 크기 줄이기: 피클 해제 중에 일부 속성이 직렬화된 형식으로 많은 공간을 차지하는 경우 바이트 스트림에 유지하는 대신 일부 속성을 다시 계산할 수 있습니다.

피클링 프로세스에 연결하려면 클래스에 다음 두 가지 특수 메서드를 지정할 수 있습니다.

  1. __getstate__()
  2. __setstate__()

Python은 클래스의 인스턴스를 피클링하기 전에 첫 번째 인스턴스를 호출하며, 메소드가 속성 이름과 해당 값의 사전을 반환할 것으로 예상합니다. 반대로, 피클링을 해제하는 동안 Python은 두 번째 메서드를 호출하고 속성 사전을 전달하여 상태가 역직렬화된 후 개체를 적절하게 초기화할 수 있도록 합니다.

In [ ]:
import time
from dataclasses import dataclass

@dataclass
class User:
    name: str
    password: str

    def __getstate__(self):
        state = self.__dict__.copy()
        state["timestamp"] = int(time.time())
        del state["password"]
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        with open("/dev/random", mode="rb") as file:
            self.password = file.read(8).decode("ascii", errors="ignore")
  • 10행은 사용자 이름과 비밀번호로 구성된 객체의 내부 속성 사전 복사본을 만듭니다.
  • 11행은 복사된 사전에 현재의 Unix 타임스탬프와 함께 timestamp라는 추가 속성을 삽입합니다.
  • 12행은 새 사전에서 사용자의 비밀번호에 해당하는 키-값 쌍을 제거합니다.
  • 13행은 개체의 수정된 상태가 포함된 새 사전을 pickle 모듈에 반환합니다.
  • 16행은 pickle 모듈에서 제공하는 역직렬화된 상태를 사용하여 객체의 내부 사전을 채웁니다.
  • 17행과 18행 Unix 계열 운영 체제의 /dev/random 파일에서 로드된 임의 바이트를 기반으로 새 비밀번호를 설정합니다.

JSON을 사용하여 객체 인코딩

모듈이 사용하는 바이너리 프로토콜과 달리pickle JSON은 사람이 읽을 수 있는 텍스트 직렬화 형식입니다. 엄청난 인기와 단순성 덕분에 다양한 프로그래밍 언어와 플랫폼에서 데이터 교환에 대해 보편적으로 받아들여지는 표준이 되었습니다.

다음 예에서는 Python 객체를 JSON 문자열로 덤프합니다.

In [ ]:
import json

data = {
    "email": None,
    "name": "John Doe",
    "age": 42.5,
    "married": True,
    "children": ["Alice", "Bob"],
}

print(json.dumps(data, indent=4, sort_keys=True))

이 경우 키가 몇 가지 다른 데이터 유형에 매핑된 문자열인 Python 사전을 직렬화합니다. 결과적으로 얻는 것은 JSON의 문법 규칙에 따라 형식화된 Python 문자열입니다. 선택적으로 출력을 보기 좋게 인쇄하고 키를 알파벳 순으로정렬 요청할 수 있습니다. 더 큰 개체의 가독성을 향상시킵니다.

JSON 형식은 6가지 기본 데이터 유형만 지원합니다.

  1. 정렬: [1, 2, 3]
  2. 부울: true, false
  3. null: null
  4. 번호: 42, 3.14
  5. object: {"key1": "value", "key2": 42}
  6. string: "Hello, World!"
In [ ]:
json.dumps({"Saturday", "Sunday"})

위의 오류 메시지는 Python 세트가 JSON 형식을 사용하여 직렬화할 수 없음을 알려줍니다. 그러나 이러한 비표준 데이터 유형이나 사용자 정의 클래스를 처리하는 방법을 json 모듈에 가르칠 수 있습니다.

두 가지 방법이 있습니다. 덤프 및 로딩 함수에 콜백 함수를 제공하거나 json 모듈에서 인코더 및 디코더 클래스를 확장할 수 있습니다.

첫 번째 접근 방식을 사용하여 Python 세트를 JSON 문자열로 직렬화하려면 다음 콜백 함수를 정의하고 해당 참조를 default 매개변수를 통해 json.dump() 또는 json.dumps()에 전달할 수 있습니다.

In [ ]:
def serialize_custom(value):
    if isinstance(value, set):
        return {
            "type": "set",
            "elements": list(value)
        }

data = {"weekend_days": {"Saturday", "Sunday"}}
json.dumps(data, default=serialize_custom)

Python은 이제 자체적으로 JSON으로 직렬화할 수 없는 모든 객체에 대해 serialize_custom()을 호출합니다. 해당 객체를 함수에 대한 인수로 전달하며, 알려진 데이터 유형 중 하나를 반환해야 합니다. 이 경우 함수는 목록으로 인코딩된 요소와 의도한 Python 데이터 유형을 나타내는 유형 필드가 포함된 사전으로 Python 세트를 나타냅니다.

In [ ]:
def deserialize_custom(value):
    match value:
        case {"type": "set", "elements": elements}:
            return set(elements)
        case _:
            return value

json_string = """
    {
        "weekend_days": {
            "type": "set",
            "elements": ["Sunday", "Saturday"]
        }
    }
"""

json.loads(json_string, object_hook=deserialize_custom)

여기서 의 매개변수를 통해 deserialize_custom()에 대한 참조를 전달합니다. 사용자 정의 함수는 Python 사전을 인수로 사용하여 이를 원하는 객체로 변환할 수 있습니다. 구조적 패턴 일치를 사용하면 사전이 집합을 설명하는지 식별하고, 그렇다면 Python 집합 인스턴스를 반환합니다. 그렇지 않으면 사전을 있는 그대로 반환합니다.

이러한 일반 콜백 외에도 몇 가지 특수 콜백을 지정하여 JSON에서 정수 및 부동 소수점 숫자 리터럴을 역직렬화하는 방법을 재정의할 수 있습니다.

In [ ]:
json.loads("42", parse_int=float)
In [ ]:
from decimal import Decimal
json.loads("3.14", parse_float=Decimal)
In [ ]:
json.loads("[NaN, Infinity, -Infinity]", parse_constant=str)

위의 코드 스니펫에서는 모든 정수를 Python 부동 소수점 수로 변환하고 모든 부동 소수점 숫자 리터럴을 십진수로 변환합니다.

In [ ]:
cycle = {}
cycle["sibling"] = {"sibling": cycle}

json.dumps(cycle)
In [ ]:
json.dumps(cycle, check_circular=False)

객체에 순환이 포함된 경우 순수 json 모듈은 이를 직렬화할 수 없습니다. 이미 직렬화한 객체를 추적하는 메커니즘을 구현하고 역직렬화 중에 메모리에 원본 객체 그래프를 재구성해야 합니다.

실행 코드 직렬화

피클 가져오기 가능 기호

In [ ]:
# plus.py

def create_plus(x):
    def plus(y):
        return x + y

    return plus

plus_one = create_plus(1)
plus_two = lambda x: x + 2
In [ ]:
import pickle
import plus
pickle.dumps(plus)
In [ ]:
pickle.dumps(plus.create_plus)

결과 바이트 스트림은 상당히 짧으며 바깥쪽 모듈 이름(plus)을 포함하여 함수의 정규화된 이름을 포함합니다. 이제 이 바이트 시퀀스를 디스크에 저장하거나 네트워크를 통해 다른 Python 인터프리터로 전송할 수 있습니다. 그러나 수신측이 해당 함수나 클래스 정의에 액세스할 수 없는 경우에도 직렬화된 코드를 해제할 수 없습니다.

In [ ]:
import importlib
import inspect

def get_module_source(module_name):
    module = importlib.import_module(module_name)
    return inspect.getsource(module)

source_code = get_module_source("plus")
print(source_code)
In [ ]:
exec(source_code)
plus_two(3)

이 트릭은 코드 직렬화를 방지하지만 그 자체로 문제가 있습니다. 모듈의 나머지 부분을 공개하지 않고 특정 기능이나 클래스만 공유하고 싶다면 어떻게 해야 할까요? 아마도 모듈을 실행하면 원치 않는 부작용이 발생할 수 있습니다. 컴파일된 C 확장 모듈이기 때문에 고급 Python 소스 코드에 액세스하지 못할 수도 있습니다.

참고: 대신 dill를 사용하면 소스 코드 범위를 다음으로 편리하게 좁힐 수 있습니다.

In [ ]:
import dill.source
from plus import plus_two
dill.source.getsource(plus_two)

람다 표현식으로 정의한 plus_two() 호출 가능 항목의 소스 코드만 가져옵니다. 그럼에도 불구하고 dill는 코드를 직렬화하는 더 나은 방법을 제공합니다.

코드 객체 직렬화

In [ ]:
import pickle
import dill
import plus

pickle.dumps(plus.create_plus)
In [ ]:
dill.dumps(plus.create_plus)

# shell python -i plus.py

create_plus()은 이제 전역 네임스페이스의 일부이므로 dill는 함수를 올바르게 직렬화할 수 있습니다. 마침내 모듈 없이 역직렬화할 수 있는 직렬화된 코드 객체를 포함하는 훨씬 더 긴 바이트 시퀀스를 출력합니다.