Construct2.9ライブラリでいくつかのバイナリデータを解析する出力をシリアル化しようとしています。結果をJSONにシリアル化したい。
packet
は、ConstructクラスContainer
のインスタンスです。
BytesIO
型の隠された__io
_が含まれているようです-以下のdict(packet)
の出力を参照してください。
_{
'packet_length': 76, 'uart_sent_time': 1, 'frame_number': 42958,
'subframe_number': 0, 'checksum': 33157, '_io': <_io.BytesIO object at 0x7f81c3153728>,
'platform':661058, 'sync': 506660481457717506, 'frame_margin': 20642,
'num_tlvs': 1, 'track_process_time': 593, 'chirp_margin': 78,
'timestamp': 2586231182, 'version': 16908293
}
_
json.dumps(packet)
を呼び出すと、明らかにTypeErrorが発生します。
_...
File "/usr/lib/python3.5/json/__init__.py", line 237, in dumps
**kw).encode(obj)
File "/usr/lib/python3.5/json/encoder.py", line 198, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/usr/lib/python3.5/json/encoder.py", line 256, in iterencode
return _iterencode(o, 0)
File "/usr/lib/python3.5/json/encoder.py", line 179, in default
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <_io.BytesIO object at 0x7f81c3153728> is not JSON serializable
_
しかし、私が混乱しているのは、json.dumps(packet, skipkeys=True)
を実行するとまったく同じエラーが発生し、__io
_フィールドをスキップすることを期待していることです。ここで問題は何ですか? skipkeys
で__io
_フィールドをスキップできないのはなぜですか?
JSONEncoder
をオーバーライドし、None
型のフィールドに対してBytesIO
を返すことでコードが機能するようになりましたが、それはつまり、シリアル化された文字列に_"_io": null
_要素の負荷が含まれることを意味しますまったく持ちたくない...
先頭に__
_アンダースコアが付いたキーは実際には「非表示」ではなく、JSONへの単なる文字列です。 Construct Container
クラスは単なる順序付きの辞書であり、__io
_キーはそのクラスにとって特別なものではありません。
次の2つのオプションがあります。
default
フックを実装します。おそらく3番目ですが、Constructプロジェクトページのカジュアルスキャンでは、利用可能かどうかはわかりません。出力JSONまたは少なくともJSON互換の辞書を、おそらくアダプターを使用して構築します。
デフォルトのフックでは、__io
_キーが出力に追加されるのを防ぐことはできませんが、少なくともエラーを回避できます:
_json.dumps(packet, default=lambda o: '<not serializable>')
_
フィルタリングは再帰的に実行できます。 @functools.singledispatch()
decorator は、このようなコードをクリーンに保つのに役立ちます。
_from functools import singledispatch
_cant_serialize = object()
@singledispatch
def json_serializable(object, skip_underscore=False):
"""Filter a Python object to only include serializable object types
In dictionaries, keys are converted to strings; if skip_underscore is true
then keys starting with an underscore ("_") are skipped.
"""
# default handler, called for anything without a specific
# type registration.
return _cant_serialize
@json_serializable.register(dict)
def _handle_dict(d, skip_underscore=False):
converted = ((str(k), json_serializable(v, skip_underscore))
for k, v in d.items())
if skip_underscore:
converted = ((k, v) for k, v in converted if k[:1] != '_')
return {k: v for k, v in converted if v is not _cant_serialize}
@json_serializable.register(list)
@json_serializable.register(Tuple)
def _handle_sequence(seq, skip_underscore=False):
converted = (json_serializable(v, skip_underscore) for v in seq)
return [v for v in converted if v is not _cant_serialize]
@json_serializable.register(int)
@json_serializable.register(float)
@json_serializable.register(str)
@json_serializable.register(bool) # redudant, supported as int subclass
@json_serializable.register(type(None))
def _handle_default_scalar_types(value, skip_underscore=False):
return value
_
上記の実装には、追加の_skip_underscore
_引数もあり、開始時に__
_文字を持つキーを明示的にスキップします。これは、Constructライブラリが使用している追加の「隠された」属性をすべてスキップするのに役立ちます。
Container
はdict
サブクラスであるため、上記のコードはpacket
などのインスタンスを自動的に処理します。
skipkeys
はあなたが思っていることをしません- json.JSONEncoder
にないキーをスキップするように指示しますbasicキーの値ではなくタイプ。つまり、dict
{object(): "foobar"}
があった場合、object()
キーはスキップされますが、skipkeys
はTrue
は、TypeError
を発生させます。
JSONEncoder.iterencode()
(およびその下腹部)をオーバーロードし、そこで先読みフィルタリングを実行できますが、最終的にはjson
モジュールを書き換えて、コンパイルされたパーツの恩恵を受けることができないため、プロセス。繰り返しフィルタリングを使用してデータを前処理し、最終的なJSONで不要なキー/タイプをスキップすることをお勧めします。その後、json
モジュールは、追加の指示なしでそれを処理できるはずです。何かのようなもの:
import collections
class SkipFilter(object):
def __init__(self, types=None, keys=None, allow_empty=False):
self.types = Tuple(types or [])
self.keys = set(keys or [])
self.allow_empty = allow_empty # if True include empty filtered structures
def filter(self, data):
if isinstance(data, collections.Mapping):
result = {} # dict-like, use dict as a base
for k, v in data.items():
if k in self.keys or isinstance(v, self.types): # skip key/type
continue
try:
result[k] = self.filter(v)
except ValueError:
pass
if result or self.allow_empty:
return result
Elif isinstance(data, collections.Sequence):
result = [] # a sequence, use list as a base
for v in data:
if isinstance(v, self.types): # skip type
continue
try:
result.append(self.filter(v))
except ValueError:
pass
if result or self.allow_empty:
return result
else: # we don't know how to traverse this structure...
return data # return it as-is, hope for the best...
raise ValueError
次に、フィルターを作成します。
import io
preprocessor = SkipFilter([io.BytesIO], ["_io"]) # double-whammy skip of io.BytesIO
この場合、タイプごとにスキップするだけで十分ですが、_io
キーが他の望ましくないデータを保持している場合は、最終結果に含まれないことが保証されます。とにかく、JSONEncoder
に渡す前にデータをフィルタリングするだけです。
import json
json_data = json.dumps(preprocessor.filter(packet)) # no _io keys or io.BytesIO data...
もちろん、構造に他のエキゾチックなデータまたはJSONでそのタイプに基づいて異なる形で表されるデータが含まれる場合、このアプローチは、すべてのマッピングをdict
に、すべてのシーケンスをlist
に変えるため、混乱する可能性があります。ただし、一般的な使用法ではこれで十分です。
シリアル化できないフィールドを無視するには、以前のすべての回答で正しく指摘されているように、重い追加のロジックが必要です。
フィールドを本当に除外する必要がない場合は、代わりにデフォルト値を生成できます。
def safe_serialize(obj):
default = lambda o: f"<<non-serializable: {type(o).__qualname__}>>"
return json.dumps(obj, default=default)
obj = {"a": 1, "b": bytes()} # bytes is non-serializable by default
print(safe_serialize(obj))
これにより、次の結果が生成されます。
{"a": 1, "b": "<<non-serializable: bytes>>"}
このコードは型名を出力します。これは、後でカスタムシリアライザーを実装する場合に役立ちます。