セットをJSONでシリアル化する方法は?


148

コレクションに重複が含まれないようにするために、メソッドとsetオブジェクトを含むPython があります。__hash____eq__

この結果をjsonでエンコードする必要がありsetますsetが、json.dumpsメソッドにempty を渡すとaが発生しTypeErrorます。

  File "/usr/lib/python2.7/json/encoder.py", line 201, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python2.7/json/encoder.py", line 264, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python2.7/json/encoder.py", line 178, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: set([]) is not JSON serializable

json.JSONEncoderカスタムdefaultメソッドを持つクラスの拡張機能を作成できることはわかっていますが、からの変換をどこから始めればよいかさえわかりませんsetsetデフォルトのメソッド内の値から辞書を作成し、そのエンコードを返す必要がありますか?理想的には、デフォルトのメソッドが、元のエンコーダーがチョークするすべてのデータ型を処理できるようにしたいと思います(Mongoをデータソースとして使用しているため、日付でもこのエラーが発生するようです)

正しい方向へのヒントがあれば幸いです。

編集:

答えてくれてありがとう!おそらくもっと正確だったはずだ。

setは翻訳の制限を回避するためにここで回答を利用(および賛成)しましたが、問題である内部キーもあります。

のオブジェクトは、にset変換される複雑なオブジェクトです__dict__が、jsonエンコーダーの基本タイプには適さない可能性のあるプロパティの値を含めることもできます。

このにはさまざまなタイプが多数setあり、ハッシュは基本的にエンティティの一意のIDを計算しますが、NoSQLの真の精神では、子オブジェクトが何を含んでいるかを正確に伝えることはできません。

1つのオブジェクトにはの日付値が含まれる場合がありますが、starts別のオブジェクトには、「非プリミティブ」オブジェクトを含むキーを含まない他のスキーマが含まれる場合があります。

そのため、考えられる唯一の解決策はJSONEncoderdefaultメソッドを置き換えてさまざまなケースをオンにするように拡張することでした。しかし、これに対処する方法がわからず、ドキュメントがあいまいです。ネストされたオブジェクトでは、defaultキーから返される値はキーによるものですか、それともオブジェクト全体を参照する一般的なinclude / discardですか?そのメソッドはネストされた値にどのように対応しますか?私は以前の質問を調べましたが、ケース固有のエンコーディングへの最良のアプローチを見つけることができないようです(残念ながら、ここで行う必要があるように思われます)。


3
なぜdictですか?私はあなたlistがセットからちょうどそれを作り、それをエンコーダーに渡したいと思うと思います...例:encode(list(myset))
Constantinius

2
JSONを使用する代わりに、YAMLを使用できます(JSONは基本的にYAMLのサブセットです)。
Paolo Moretti

@PaoloMoretti:それでもメリットはありますか?セットはYAMLの普遍的にサポートされているデータ型の1つではないと思います。特にAPIに関しては、それほど広くサポートされていません。

@PaoloMorettiご入力ありがとうございます。ただし、アプリケーションのフロントエンドでは戻り値の型としてJSONが必要であり、この要件はあらゆる目的で修正されています。
DeaconDesperado

2
@delnan セット日付の両方をネイティブでサポートしているため、YAMLを提案しました。
Paolo Moretti

回答:


116

JSON表記には少数のネイティブデータ型(オブジェクト、配列、文​​字列、数値、ブール値、およびnull)しかないため、JSONでシリアル化されたものはすべて、これらの型の1つとして表現する必要があります。

jsonモジュールのドキュメントに示されているように、この変換はJSONEncoderJSONDecoderによって自動的に実行できますが、必要な他の構造をあきらめることになります(セットをリストに変換すると、通常の状態に戻すことができなくなりますリスト;を使用してセットを辞書に変換すると、辞書をdict.fromkeys(s)復元できなくなります)。

より洗練されたソリューションは、他のネイティブJSON型と共存できるカスタム型を構築することです。これにより、リスト、セット、辞書、小数、日時オブジェクトなどを含むネストされた構造を保存できます。

from json import dumps, loads, JSONEncoder, JSONDecoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, unicode, int, float, bool, type(None))):
            return JSONEncoder.default(self, obj)
        return {'_python_object': pickle.dumps(obj)}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(str(dct['_python_object']))
    return dct

リスト、辞書、およびセットを処理できることを示すサンプルセッションを次に示します。

>>> data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]

>>> j = dumps(data, cls=PythonObjectEncoder)

>>> loads(j, object_hook=as_python_object)
[1, 2, 3, set(['knights', 'say', 'who', 'ni']), {u'key': u'value'}, Decimal('3.14')]

または、YAMLTwisted Jelly、Pythonのpickleモジュールなど、より汎用的なシリアル化手法を使用すると便利な場合があります。これらはそれぞれ、より広範囲のデータ型をサポートしています。


11
これは、YAMLがJSONよりも一般的な目的であると聞いた最初のものです... o_O
Karl Knechtel

13
@KarlKnechtel YAMLはJSONのスーパーセットです(非常に近い)。また、バイナリデータ、セット、順序付けられたマップ、およびタイムスタンプのタグも追加します。より多くのデータ型をサポートすることは、「より汎用的な」という意味です。別の意味で「一般目的」という言葉を使っているようです。
レイモンドヘッティンガー2012年

4
jsonpickleも忘れないでください。jsonpickleは、PythonオブジェクトをJSONにピクルするための一般化されたライブラリであることを目的としています。
ジェイソンR.クームス2013

4
バージョン1.2以降、YAMLはJSONの厳密なスーパーセットです。すべての正当なJSONが正当なYAMLになりました。 yaml.org/spec/1.2/spec.html
steveha

2
このコード例はインポートしますJSONDecoderが使用しません
watsonic 2015

115

listに遭遇したときにを返すカスタムエンコーダーを作成できますset。次に例を示します。

>>> import json
>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5]), cls=SetEncoder)
'[1, 2, 3, 4, 5]'

この方法でも他のタイプを検出できます。リストが実際にはセットであることを保持する必要がある場合は、カスタムエンコーディングを使用できます。のようなものreturn {'type':'set', 'list':list(obj)}がうまくいくかもしれません。

ネストされた型を示すために、これをシリアル化することを検討してください:

>>> class Something(object):
...    pass
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)

これにより、次のエラーが発生します。

TypeError: <__main__.Something object at 0x1691c50> is not JSON serializable

これは、エンコーダーがlist返された結果を受け取り、その子でシリアライザーを再帰的に呼び出すことを示します。複数のタイプのカスタムシリアライザーを追加するには、次のようにします。

>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       if isinstance(obj, Something):
...          return 'CustomSomethingRepresentation'
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)
'[1, 2, 3, 4, 5, "CustomSomethingRepresentation"]'

おかげで、質問を編集して、これが私が必要とするタイプのタイプであることをより明確に示しました。私が理解できないように見えるのは、このメソッドがネストされたオブジェクトをどのように処理するかです。あなたの例では、戻り値はセットのリストですが、渡されたオブジェクトが日付(別の不正なデータ型)を含むセットであった場合はどうなりますか?デフォルトのメソッド自体の中でキーをドリルスルーする必要がありますか?トンありがとう!
DeaconDesperado

1
JSONモジュールはネストされたオブジェクトを処理すると思います。リストを取得すると、それぞれをエンコードしようとするリスト項目を繰り返し処理します。それらの1つが日付である場合、default関数は再び呼び出されますが、今回objは日付オブジェクトであるため、テストして日付表現を返すだけです。
jterrace

したがって、デフォルトのメソッドは、渡された任意の1つのオブジェクトに対して複数回実行される可能性があります。「リスト化」されると、個々のキーも調べるためです。
DeaconDesperado

つまり、同じオブジェクトに対して複数回呼び出されることはありませんが、子に再帰できます。更新された回答を参照してください。
jterrace、

あなたが説明したとおりに機能しました。私はまだいくつかの欠点を理解する必要がありますが、そのほとんどはおそらくリファクタリングできるものです。ご指導ありがとうございました!
DeaconDesperado

7

レイモンドヘッティンガーのソリューションをpython 3 に適合させました。

変更点は次のとおりです。

  • unicode 消えた
  • 両親への呼び出しを更新しdefaultsuper()
  • タイプbase64をシリアル化するために使用する(Python 3ではJSONに変換できないように見えるため)bytesstrbytes
from decimal import Decimal
from base64 import b64encode, b64decode
from json import dumps, loads, JSONEncoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, int, float, bool, type(None))):
            return super().default(obj)
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
    return dct

data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]
j = dumps(data, cls=PythonObjectEncoder)
print(loads(j, object_hook=as_python_object))
# prints: [1, 2, 3, {'knights', 'who', 'say', 'ni'}, {'key': 'value'}, Decimal('3.14')]

4
関連する質問に対するこの回答の最後に示されているコードは、必要でないものをスキップして、byteオブジェクトのjson.dumps()returnから/へのバイトオブジェクトをデコードおよびエンコードするだけで同じことを実現します。'latin1'base64
martineau

6

JSONで使用できるのは、辞書、リスト、およびプリミティブオブジェクトタイプ(int、string、bool)のみです。


5
「プリミティブオブジェクトタイプ」は、Pythonに関しては意味がありません。「組み込みオブジェクト」はより理にかなっていますが、ここでは広すぎます(初心者には、dict、リスト、セットも含まれます)。(ただし、JSONの用語は異なる場合があります。)

文字列番号オブジェクト配列true false null
Joseph Le Brech

6

defaultメソッドを提供するためにカスタムエンコーダークラスを作成する必要はありません。これはキーワード引数として渡すことができます。

import json

def serialize_sets(obj):
    if isinstance(obj, set):
        return list(obj)

    return obj

json_str = json.dumps(set([1,2,3]), default=serialize_sets)
print(json_str)

[1, 2, 3]サポートされているすべてのPythonバージョンになります。


4

一般的なPythonオブジェクトではなくセットのみをエンコードする必要があり、それを人間が読めるように簡単に保持したい場合は、レイモンドヘッティンガーの回答の簡易版を使用できます。

import json
import collections

class JSONSetEncoder(json.JSONEncoder):
    """Use with json.dumps to allow Python sets to be encoded to JSON

    Example
    -------

    import json

    data = dict(aset=set([1,2,3]))

    encoded = json.dumps(data, cls=JSONSetEncoder)
    decoded = json.loads(encoded, object_hook=json_as_python_set)
    assert data == decoded     # Should assert successfully

    Any object that is matched by isinstance(obj, collections.Set) will
    be encoded, but the decoded value will always be a normal Python set.

    """

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        else:
            return json.JSONEncoder.default(self, obj)

def json_as_python_set(dct):
    """Decode json {'_set_object': [1,2,3]} to set([1,2,3])

    Example
    -------
    decoded = json.loads(encoded, object_hook=json_as_python_set)

    Also see :class:`JSONSetEncoder`

    """
    if '_set_object' in dct:
        return set(dct['_set_object'])
    return dct

1

クイックダンプだけが必要で、カスタムエンコーダーを実装したくない場合。次のものを使用できます。

json_string = json.dumps(data, iterable_as_array=True)

これにより、すべてのセット(およびその他の反復可能オブジェクト)が配列に変換されます。jsonを解析して戻すときに、これらのフィールドが配列のままになることに注意してください。タイプを保持する場合は、カスタムエンコーダーを作成する必要があります。


7
TypeError:__init __()が予期しないキーワード引数 'iterable_as_array'を取得しました
atm

simplejsonをインストールする必要があります
JerryBringer

simplejsonをjsonとしてインポートし、次にjson_string = json.dumps(data、iterable_as_array = True)はPython 3.6でうまく機能します
fraverta

1

受け入れられているソリューションの 1つの欠点は、その出力が非常にpython固有であることです。つまり、JSONの生の出力を人間が観察したり、別の言語(JavaScriptなど)で読み込んだりすることはできません。例:

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)

あなたを得るでしょう:

{"a": [44, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsESwVLBmWFcQJScQMu"}], "b": [55, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsCSwNLBGWFcQJScQMu"}]}

セットをダウンリストのリストを含む辞書にダウングレードし、同じエンコーダーを使用してpythonにロードするとセットに戻るため、可観測性と言語にとらわれない方法を維持するソリューションを提案できます。

from decimal import Decimal
from base64 import b64encode, b64decode
from json import dumps, loads, JSONEncoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, int, float, bool, type(None))):
            return super().default(obj)
        elif isinstance(obj, set):
            return {"__set__": list(obj)}
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

def as_python_object(dct):
    if '__set__' in dct:
        return set(dct['__set__'])
    elif '_python_object' in dct:
        return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
    return dct

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)
ob = loads(j)
print(ob["a"])

あなたを得る:

{"a": [44, {"__set__": [4, 5, 6]}], "b": [55, {"__set__": [2, 3, 4]}]}
[44, {'__set__': [4, 5, 6]}]

キーを持つ要素を持つディクショナリをシリアル化すると、"__set__"このメカニズムが無効になることに注意しください。その__set__ため、予約済みのdictキーになりました。明らかに、より難読化された別のキーを自由に使用してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.