program story

일반 인코더로 객체 JSON을 직렬화 가능하게 만들기

inputbox 2020. 12. 25. 09:43
반응형

일반 인코더로 객체 JSON을 직렬화 가능하게 만들기


직렬화 불가능한 커스텀 객체를 JSON으로 직렬화하는 일반적인 방법은 서브 클래스 json.JSONEncoder를 만든 다음 커스텀 인코더를 덤프에 전달하는 것입니다.

일반적으로 다음과 같습니다.

class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, foo):
            return obj.to_json()

        return json.JSONEncoder.default(self, obj)

print json.dumps(obj, cls = CustomEncoder)

내가하려는 것은 기본 인코더로 직렬화 가능한 것을 만드는 것입니다. 주위를 둘러 보았지만 아무것도 찾지 못했습니다. 내 생각에는 인코더가 json 인코딩을 결정하기 위해 살펴 보는 필드가있을 것이라고 생각합니다. 유사한 __str__. 아마도 __json__필드 일 것입니다. 파이썬에 이와 같은 것이 있습니까?

패키지를 사용하는 모든 사용자가 자신의 [사소한] 사용자 지정 인코더 구현에 대해 걱정하지 않고 JSON 직렬화가 가능하도록 만드는 모듈의 한 클래스를 만들고 싶습니다.


질문에 대한 의견에서 말했듯이 json모듈의 소스 코드를 살펴본 후 원하는 작업을 수행하는 데 적합하지 않은 것 같습니다. 그러나이 목표는 원숭이 패치 ( Monkey Patching) 로 알려진 것에 의해 달성 될 수 있습니다 (질문 원숭이 패치 란 무엇입니까? ). 이는 패키지의 __init__.py초기화 스크립트 에서 수행 할 수 있으며 json모듈은 일반적으로 한 번만로드되고 결과가 .NET에 캐시되기 때문에 모든 후속 모듈 직렬화에 영향을줍니다 sys.modules.

패치는 기본 json 인코더의 default방법 (기본값 default().

다음은 단순성을 위해 독립형 모듈로 구현 된 예입니다.

기준 치수: make_json_serializable.py

""" Module that monkey-patches json module when it's imported so
JSONEncoder.default() automatically checks for a special "to_json()"
method and uses it to encode the object if found.
"""
from json import JSONEncoder

def _default(self, obj):
    return getattr(obj.__class__, "to_json", _default.default)(obj)

_default.default = JSONEncoder.default  # Save unmodified default.
JSONEncoder.default = _default # Replace it.

단순히 모듈을 가져 와서 패치를 적용하기 때문에 사용은 간단합니다.

샘플 클라이언트 스크립트 :

import json
import make_json_serializable  # apply monkey-patch

class Foo(object):
    def __init__(self, name):
        self.name = name
    def to_json(self):  # New special method.
        """ Convert to JSON format string representation. """
        return '{"name": "%s"}' % self.name

foo = Foo('sazpaz')
print(json.dumps(foo))  # -> "{\"name\": \"sazpaz\"}"

객체 유형 정보를 유지하기 위해 특수 메서드는 반환 된 문자열에이를 포함 할 수도 있습니다.

        return ('{"type": "%s", "name": "%s"}' %
                 (self.__class__.__name__, self.name))

이제 클래스 이름을 포함하는 다음 JSON을 생성합니다.

"{\"type\": \"Foo\", \"name\": \"sazpaz\"}"

Magick는 여기에있다

특별한 메서드를 추가하지 않고도 사용자 정의 클래스 인스턴스를 포함하여 default()대부분의 Python 객체를 자동으로 직렬화 할 수 있다는 점이 특별히 명명 된 메서드를 대체하는 것보다 더 좋습니다 . 여러 대안을 조사한 후, pickle모듈 을 사용하는 다음은 저에게 가장 이상적인 것 같았습니다.

기준 치수: make_json_serializable2.py

""" Module that imports the json module and monkey-patches it so
JSONEncoder.default() automatically pickles any Python objects
encountered that aren't standard JSON data types.
"""
from json import JSONEncoder
import pickle

def _default(self, obj):
    return {'_python_object': pickle.dumps(obj)}

JSONEncoder.default = _default  # Replace with the above.

물론 모든 것을 절인 할 수는 없습니다 (예 : 확장 유형). 그러나 여러분이 제안하고 앞서 설명한 것과 유사한 특수 방법을 작성하여 피클 프로토콜을 통해이를 처리하도록 정의 된 방법이 있지만 훨씬 적은 수의 경우에 필요할 수 있습니다.

그럼에도 불구하고 pickle 프로토콜 을 사용하면 전달 된 사전의 키 를 사용하는 object_hook모든 json.loads()호출 에 대해 사용자 정의 함수 인수를 제공하여 원래 Python 객체를 재구성하는 것이 매우 쉽습니다 '_python_object'. 다음과 같은 것 :

def as_python_object(dct):
    try:
        return pickle.loads(str(dct['_python_object']))
    except KeyError:
        return dct

pyobj = json.loads(json_str, object_hook=as_python_object)

이 작업을 여러 곳에서 수행해야하는 경우 추가 키워드 인수를 자동으로 제공하는 래퍼 함수를 ​​정의하는 것이 좋습니다.

json_pkloads = functools.partial(json.loads, object_hook=as_python_object)

pyobj = json_pkloads(json_str)

당연히 이것은 json모듈에 원숭이 패치를 적용 하여 함수를 기본값으로 만들 수 있습니다 object_hook(대신 None).

내가 사용하는 생각이있어 pickle에서 대답 하여 레이몬드 Hettinger 나는 매우 신뢰할 고려 또 다른 JSON 직렬화 문제뿐만 아니라 (파이썬 코어 개발에서와 같이) 공식 소스.

Python 3으로의 이식성

위 코드는에서 처리 할 수없는 객체를 json.dumps()반환 하기 때문에 Python 3에서와 같이 작동하지 않습니다 . 그러나 접근 방식은 여전히 ​​유효합니다. 이 문제를 해결하는 간단한 방법은 반환 된 값 "디코딩" 한 다음 함수 에서 전달하기 전에 "인코딩" 하는 것입니다 . 이것은 항상 유니 코드로 디코딩 된 다음 다시 원래 문자열로 인코딩 될 수있는 임의의 바이너리 문자열이 유효하기 때문에 작동 합니다 ( 이 답변 에서 Sven Marnach 에서 지적 했듯이 ).bytesJSONEncoderlatin1pickle.dumps()latin1pickle.loads()as_python_object()latin1

(다음은 Python 2에서 잘 작동하지만 latin1디코딩 및 인코딩은 불필요합니다.)

from decimal import Decimal

class PythonObjectEncoder(json.JSONEncoder):
    def default(self, obj):
        return {'_python_object': pickle.dumps(obj).decode('latin1')}

def as_python_object(dct):
    try:
        return pickle.loads(dct['_python_object'].encode('latin1'))
    except KeyError:
        return dct

data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'},
        Decimal('3.14')]
j = json.dumps(data, cls=PythonObjectEncoder, indent=4)
data2 = json.loads(j, object_hook=as_python_object)
assert data == data2  # both should be same

다음과 같이 dict 클래스를 확장 할 수 있습니다.

#!/usr/local/bin/python3
import json

class Serializable(dict):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # hack to fix _json.so make_encoder serialize properly
        self.__setitem__('dummy', 1)

    def _myattrs(self):
        return [
            (x, self._repr(getattr(self, x))) 
            for x in self.__dir__() 
            if x not in Serializable().__dir__()
        ]

    def _repr(self, value):
        if isinstance(value, (str, int, float, list, tuple, dict)):
            return value
        else:
            return repr(value)

    def __repr__(self):
        return '<%s.%s object at %s>' % (
            self.__class__.__module__,
            self.__class__.__name__,
            hex(id(self))
        )

    def keys(self):
        return iter([x[0] for x in self._myattrs()])

    def values(self):
        return iter([x[1] for x in self._myattrs()])

    def items(self):
        return iter(self._myattrs())

이제 일반 인코더를 사용하여 클래스를 직렬화 할 수 있도록 'Serializable'을 확장합니다.

class MySerializableClass(Serializable):

    attr_1 = 'first attribute'
    attr_2 = 23

    def my_function(self):
        print('do something here')


obj = MySerializableClass()

print(obj) 다음과 같이 인쇄됩니다.

<__main__.MySerializableClass object at 0x1073525e8>

print(json.dumps(obj, indent=4)) 다음과 같이 인쇄됩니다.

{
    "attr_1": "first attribute",
    "attr_2": 23,
    "my_function": "<bound method MySerializableClass.my_function of <__main__.MySerializableClass object at 0x1073525e8>>"
}

클래스 정의에 해킹을 넣는 것이 좋습니다. 이런 식으로 클래스가 정의되면 JSON을 지원합니다. 예:

import json

class MyClass( object ):

    def _jsonSupport( *args ):
        def default( self, xObject ):
            return { 'type': 'MyClass', 'name': xObject.name() }

        def objectHook( obj ):
            if 'type' not in obj:
                return obj
            if obj[ 'type' ] != 'MyClass':
                return obj
            return MyClass( obj[ 'name' ] )
        json.JSONEncoder.default = default
        json._default_decoder = json.JSONDecoder( object_hook = objectHook )

    _jsonSupport()

    def __init__( self, name ):
        self._name = name

    def name( self ):
        return self._name

    def __repr__( self ):
        return '<MyClass(name=%s)>' % self._name

myObject = MyClass( 'Magneto' )
jsonString = json.dumps( [ myObject, 'some', { 'other': 'objects' } ] )
print "json representation:", jsonString

decoded = json.loads( jsonString )
print "after decoding, our object is the first in the list", decoded[ 0 ]

재정의의 문제점 JSONEncoder().default은 한 번만 수행 할 수 있다는 것입니다. 해당 패턴에서 작동하지 않는 특수 데이터 유형을 발견 한 경우 (예 : 이상한 인코딩을 사용하는 경우). 아래 패턴을 사용하면 직렬화하려는 클래스 필드가 자체적으로 직렬화 가능하고 Python 목록에 거의 아무것도 추가 할 수없는 경우 항상 클래스 JSON을 직렬화 할 수 있습니다. 그렇지 않으면 json 필드에 동일한 패턴을 재귀 적으로 적용해야합니다 (또는 여기에서 직렬화 가능한 데이터를 추출).

# base class that will make all derivatives JSON serializable:
class JSONSerializable(list): # need to derive from a serializable class.

  def __init__(self, value = None):
    self = [ value ]

  def setJSONSerializableValue(self, value):
    self = [ value ]

  def getJSONSerializableValue(self):
    return self[1] if len(self) else None


# derive  your classes from JSONSerializable:
class MyJSONSerializableObject(JSONSerializable):

  def __init__(self): # or any other function
    # .... 
    # suppose your__json__field is the class member to be serialized. 
    # it has to be serializable itself. 
    # Every time you want to set it, call this function:
    self.setJSONSerializableValue(your__json__field)
    # ... 
    # ... and when you need access to it,  get this way:
    do_something_with_your__json__field(self.getJSONSerializableValue())


# now you have a JSON default-serializable class:
a = MyJSONSerializableObject()
print json.dumps(a)

I don't understand why you can't write a serialize function for your own class? You implement the custom encoder inside the class itself and allow "people" to call the serialize function that will essentially return self.__dict__ with functions stripped out.

edit:

This question agrees with me, that the most simple way is write your own method and return the json serialized data that you want. They also recommend to try jsonpickle, but now you're adding an additional dependency for beauty when the correct solution comes built in.


For production environment, prepare rather own module of json with your own custom encoder, to make it clear that you overrides something. Monkey-patch is not recommended, but you can do monkey patch in your testenv.

For example,

class JSONDatetimeAndPhonesEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime.date, datetime.datetime)):
            return obj.date().isoformat()
        elif isinstance(obj, basestring):
            try:
                number = phonenumbers.parse(obj)
            except phonenumbers.NumberParseException:
                return json.JSONEncoder.default(self, obj)
            else:
                return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.NATIONAL)
        else:
            return json.JSONEncoder.default(self, obj)

you want:

payload = json.dumps(your_data, cls=JSONDatetimeAndPhonesEncoder)

or:

payload = your_dumps(your_data)

or:

payload = your_json.dumps(your_data)

however in testing environment, go a head:

@pytest.fixture(scope='session', autouse=True)
def testenv_monkey_patching():
    json._default_encoder = JSONDatetimeAndPhonesEncoder()

which will apply your encoder to all json.dumps occurrences.

ReferenceURL : https://stackoverflow.com/questions/18478287/making-object-json-serializable-with-regular-encoder

반응형