シリアライズ の概念は、 保存したり、再利用したり、他の人に送信したいデータをメモリ上に持っているとしよう。 これは、そのデータをどのように保存したいのか、どのように再利用したいのか、だれに送りたいのかによって違う。 多くのゲームは終了時に「セーブ」を行い、ゲームを再び起動したときに以前の場所から再開できるようになっている(実際には、ゲーム以外の多くのアプリケーションも似たようなことを行っている)。
この場合、ゲームの終了時にどこまで進んだかを記録するデータ構造がディスクに保存され、再び起動するときにディスクから読み込まれる必要がある。 このデータは、作成したのと同じアプリケーションによって読み込まれるだけで、ネットワーク越しに転送されたり、作成したアプリケーション以外によって読み込まれることは無いという想定で作られている。
したがって、相互運用性に関して考慮すべきことは、 古いバージョンのプログラムが作成したデータを、新しいバージョンのプログラムが読み込めることを保証することに限られる。
このような場合には pickle
モジュールが使える。
これはPython標準ライブラリに含まれているのでいつでも利用できるし、Pythonインタプリタと同様に大部分がC言語で書かれているので処理も速い。
そして、どんなに複雑なPythonのデータ構造でも保存することができる。
pickleモジュールは何を保存できるのか?
これでも不十分な場合は、pickleモジュールを拡張することもできる。
pickleモジュールはデータ構造を扱う。
ここでやりたいことは、Atomフィードのエントリのような何か意味のあるデータを収めたPythonの辞書を作ること。 この辞書が様々なデータ型を格納している。 これらの値の内容については気にしなくて良い。
entry = {}
entry['title'] = 'Dive into history, 2009 edition'
entry['article_link'] = 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'
entry['comments_link'] = None
entry['internal_id'] = b'\xDE\xD5\xB4\xF8'
entry['tags'] = ('diveintopython', 'docbook', 'html')
entry['published'] = True
time
モジュールは、時刻上の1点(ミリ秒までの精度を持つ)を表現するためのデータ構造 (time_struct
) と、そのデータ構造を操作するための関数を含んでいる。
strptime()
関数は、フォーマットされた文字列を受け取って
time_struct
に変換する。
この文字列はデフォルトの形式で表現されているが、フォーマットコードによって制御することもできる。
import time
entry['published_date'] = time.strptime('Fri Mar 27 22:20:42 2009')
entry['published_date']
time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1)
entry
{'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', 'comments_link': None, 'internal_id': b'\xde\xd5\xb4\xf8', 'published': True, 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1), 'tags': ('diveintopython', 'docbook', 'html'), 'title': 'Dive into history, 2009 edition'}
これをファイルに保存する。
import pickle
pickle
<module 'pickle' from 'C:\\Miniconda3\\lib\\pickle.py'>
open()
関数を使ってファイルを開く。
ファイルをバイナリモードで開くために、ファイルモードは'wb'
に設定する。
これをwith
文で包むことで、
保存が終わったときに自動的にファイルが閉じられるようにしておく。
pickle
モジュールの dump()
関数は、
シリアライズ可能なPythonのデータ構造を1つ受け取り、
それを最新のバージョンの Pickle
プロトコルを用いて
Python固有のバイナリ形式にシリアライズした上で、開いておいたファイルにそれを保存する。
with open('entry.pickle', 'wb') as f:
pickle.dump(entry, f)
pickleモジュールはPythonのデータ構造を受け取り、それをファイルに保存する。
これを行うために、このモジュールは「Pickleプロトコル」と呼ばれる形式を用いてデータ構造を「シリアライズ」する。
PickleプロトコルはPython固有のものであり、
他の言語で使用できる保証は無い。
Perl、php、Java、その他のいかなる言語であっても、
entry.pickle
ファイルを有効に使うことはできないだろう。
Pythonに新しい型が追加されるたびにPickleプロトコルには何度も変更が加えられてきているが、制限は依然として存在する。
古いバージョンのPythonは(新しい型をサポートしていないので)新しいシリアライズ形式をサポートしない。
特に指定しない限り、pickleモジュールの関数は、最新バージョンのPickleプロトコルを使用する。このおかげでシリアライズできるデータ型の範囲は最も広くなるのだが、その代償として、最新のPickleプロトコルをサポートしていない古いバージョンのPythonでは生成されるPickleファイルを読み込めなくなってしまう。
最新バージョンのPickleプロトコルはバイナリ形式だ。Pickleファイルがバイナリモードで開かれていることを確認しよう。さもなければ、データは書き込み中に破損してしまうだろう。
pickleモジュールはバイナリ形式を使うので、Pickleファイルは常にバイナリモードで開かなければならない。
pickle.load()
関数は、ストリームオブジェクトを受け取り、ストリームからシリアライズされたデータを読み込み、新しいPythonオブジェクトを作り、
シリアライズされたデータを新しいPythonオブジェクトの中に再構成し、
その新しいPythonオブジェクトを返す。
with open('entry.pickle', 'rb') as f:
entry_load = pickle.load(f)
entry_load
変数は、見覚えのあるキーと値を持つ辞書になった。
entry_load
{'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', 'comments_link': None, 'internal_id': b'\xde\xd5\xb4\xf8', 'published': True, 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1), 'tags': ('diveintopython', 'docbook', 'html'), 'title': 'Dive into history, 2009 edition'}
pickle.dump()
/ pickle.load()
のサイクルによって、元のデータ構造と等しい内容をもつ新たなデータ構造がもたらされる。
entry.pickle
ファイルを開いて、シリアライズされたデータを新しい変数entry2に読み込む。
with open('entry.pickle', 'rb') as f:
entry2 = pickle.load(f)
2つの辞書 entry
と entry2
が等しいことをPythonが認めている。
このシェルでは、空の辞書からスタートし、特定のキーに手作業で値を設定することでentryを組み上げた。
そして、この辞書をシリアライズし、entry.pickleファイルに保存した。
さらに、そのファイルからシリアライズされたデータを読み込んで、元のデータ構造の完全な複製を作成した。
entry == entry2
True
等しいことと同一であることは違う。 元のデータ構造の完全な複製を作った。 これは正しい。しかし、これはコピーに過ぎない。
entry is entry2
False
ここで、
entry2['tags']
('diveintopython', 'docbook', 'html')
entry2['internal_id']
b'\xde\xd5\xb4\xf8'
Pythonオブジェクトをディスク上のファイルに直接シリアライズする方法を示していた。
しかし、ファイルが必要ない場合や、ファイルを使わずにやりたい場合はどうすればいいのだろうか?
pickle
は、メモリ上のbytesオブジェクトにシリアライズすることもできる。
pickle.dumps()
関数(関数名の末尾に's'が付いていることに注意)は、
pickle.dump()
関数と同様のシリアライズを行う。
ただし、ストリームオブジェクトを受け取って、
ディスク上のファイルにシリアライズしたデータを書き込む代わりに、
ただ単にシリアライズしたデータを返す。
b = pickle.dumps(entry)
Pickleプロトコルはバイナリ形式を用いるので、
pickle.dumps()
関数は bytes
オブジェクトを返す。
type(b)
bytes
pickle.loads()
関数(再び、関数名の末尾に's'が付いていることに注意)は、
pickle.load()
関数と同様のデシリアライズを行う。
ただし、ストリームオブジェクトを受け取って、ファイルからシリアライズされたデータを読み込む代わりに、
pickle.dumps()
関数が生成するような、シリアライズされたデータを含んだbytesオブジェクトを受け取る。
entry3 = pickle.loads(b)
元の辞書の完全な複製になる。
entry3 == entry
True
Pickleプロトコルが最初に考案されたのは何年も前のことであり、 Pythonが言語として成熟するのにあわせて、このプロトコルも成長してきた。 現在は4つの異なるバージョンのPickleプロトコルが存在する。
Python 1.xは、2つのPickleプロトコルを持っていた。
Python 2.3では、Pythonのクラスオブジェクトの新機能を扱うために、
これはバイナリ形式。
Python 3.0では、bytesオブジェクトとバイト配列の明示的なサポートをもつ、
これはバイナリ形式。
つまり、 Python 3はプロトコルバージョン2でPickle化されたデータを読み込むことができるが、 Python 2はプロトコルバージョン3でPickle化されたデータを読み込むことができない。
Pickleプロトコルはどのような姿をしているのだろうか?
ファイルを逆アセンブルしたものだが、最も興味深い情報は最後の行に含まれている。 というのも、この行にはファイルを保存したときに使ったPickleプロトコルのバージョンが記されているからだ。 Pickleプロトコルには、バージョンを示すための明示的なマーカが存在しない。 したがって、ファイルの保存にどのバージョンのプロトコルが用いられたのかを知るためには、Pickle化されたデータの中にある目印(“opcodes”)に目を向けて、 「どのopcodeがどのバージョンのPickleプロトコルで導入されたのか」というハードコードされた知識をもとに推測を行うしかない。
pickletools.dis()
関数もこの方法でバージョンを識別しており、
その結果を逆アセンブリ出力の最後の行に表示してくれる。
import pickletools
with open('entry.pickle', 'rb') as f:
pickletools.dis(f)
0: \x80 PROTO 3 2: } EMPTY_DICT 3: q BINPUT 0 5: ( MARK 6: X BINUNICODE 'article_link' 23: q BINPUT 1 25: X BINUNICODE 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition' 104: q BINPUT 2 106: X BINUNICODE 'published_date' 125: q BINPUT 3 127: c GLOBAL 'time struct_time' 145: q BINPUT 4 147: ( MARK 148: M BININT2 2009 151: K BININT1 3 153: K BININT1 27 155: K BININT1 22 157: K BININT1 20 159: K BININT1 42 161: K BININT1 4 163: K BININT1 86 165: J BININT -1 170: t TUPLE (MARK at 147) 171: q BINPUT 5 173: } EMPTY_DICT 174: q BINPUT 6 176: \x86 TUPLE2 177: q BINPUT 7 179: R REDUCE 180: q BINPUT 8 182: X BINUNICODE 'title' 192: q BINPUT 9 194: X BINUNICODE 'Dive into history, 2009 edition' 230: q BINPUT 10 232: X BINUNICODE 'internal_id' 248: q BINPUT 11 250: C SHORT_BINBYTES b'\xde\xd5\xb4\xf8' 256: q BINPUT 12 258: X BINUNICODE 'comments_link' 276: q BINPUT 13 278: N NONE 279: X BINUNICODE 'tags' 288: q BINPUT 14 290: X BINUNICODE 'diveintopython' 309: q BINPUT 15 311: X BINUNICODE 'docbook' 323: q BINPUT 16 325: X BINUNICODE 'html' 334: q BINPUT 17 336: \x87 TUPLE3 337: q BINPUT 18 339: X BINUNICODE 'published' 353: q BINPUT 19 355: \x88 NEWTRUE 356: u SETITEMS (MARK at 5) 357: . STOP highest protocol among opcodes = 3
次に示すのは、何も表示することなく、単にバージョン番号のみを返す関数だ
import pickletools
def protocol_version(file_object):
maxproto = -1
for opcode, arg, pos in pickletools.genops(file_object):
maxproto = max(maxproto, opcode.proto)
return maxproto
with open('entry.pickle', 'rb') as f:
v = protocol_version(f)
print(v)
3
pickle
モジュールが使っているデータ形式はPython固有のものであり、他のプログラミング言語との互換性をとる努力は行われていない。
言語間の互換性が必要な場合は、他のシリアライズ形式に目を向ける必要がある。
そのような形式の1つとしてjson
がある。
“json” は “JavaScript Object Notation” の略だが、
この名前に騙されてはいけない。jsonは、様々な言語間で使えるように明確に設計されている。
Python 3の標準ライブラリの中には json
モジュールが含まれている。
json
モジュールは、pickle
モジュールと同様に、
を持っている。
しかし、重要な違いもいくつか存在する。
RFC 4627は、jsonの形式と、個々の異なる型がどのようにテキストとしてエンコードされるのかを定義している。 例えばブール値は、5文字の文字列'false'、もしくは4文字の文字列'true'として保存される。 jsonのすべての値は、大文字と小文字が区別される。
このホワイトスペースは「意味を持たない」とされている。
すなわち、jsonのエンコーダは好きなようにホワイトスペースを加えることができるし、jsonのデコーダはホワイトスペースを無視するよう仕様で定められているのだ。これによって、jsonのデータを “pretty-print”、つまり、異なるレベルのインデントを加えて、値を値の中にきれいにネストさせて出力できるので、
標準的なブラウザやテキストエディタで容易にこのデータを読むことができる。
Pythonのjsonモジュールは、エンコード時に “pretty-print”
を行うオプションを持っている。
json
はプレーンテキストとして値を保存するが、知っての通り「プレーンテキスト」などというものは存在しない。
json
は、Unicodeエンコーディング(UTF-32・UTF-16・utf-8。デフォルトはutf-8)で保存されなければならない。
RFC 4627の第3節は、どのエンコーディングが使われているのかを見分ける方法を定義している。
json
は、JavaScriptで手作業で定義するようなデータ構造と非常によく似ている。
これは実際に、jsonでシリアライズされたデータをJavaScriptのeval()関数を使って「デコード」することができる。
新しいデータ構造を定義する。
basic_entry = {}
basic_entry['id'] = 256
basic_entry['title'] = 'Dive into history, 2009 edition'
basic_entry['tags'] = ('diveintopython', 'docbook', 'html')
basic_entry['published'] = True
basic_entry['comments_link'] = None
jsonはテキストベースの形式だ。つまり、このファイルはテキスト形式で開かなければならず、文字コードも指定する必要がある。utf-8を使っておけば問題は決して起きない。
pickle
モジュールと同様に、jsonモジュールは、Pythonのデータ構造と書き込み可能なストリームオブジェクトを受け取る dump()
関数を定義している。dump()
関数はPythonのデータ構造をシリアライズして、
それをストリームオブジェクトに書き込む。
これをwith文の中で行うことによって、処理が終わったときにファイルが適切に閉じられることを保証している。
import json
with open('basic.json', mode='w', encoding='utf-8') as f:
json.dump(basic_entry, f)
jsonの中身を表示する。
with open('basic.json', mode='r', encoding='utf-8') as f:
print(f.read())
{"id": 256, "published": true, "comments_link": null, "tags": ["diveintopython", "docbook", "html"], "title": "Dive into history, 2009 edition"}
jsonは、値のあいだに任意個のホワイトスペースを入れることができるので、 jsonモジュールはこの長所を生かして、さらに読みやすいjsonファイルを手軽に作る方法を提供している。
indentパラメータをjson.dump()
関数に渡すと、
jsonファイルをより読みやすい形に整形して出力してくれる(その代わりに、ファイルサイズは増える)。
indent
パラメータは整数になる。
これを0にすると「各々の値ごとに1行を使う」という意味になる。
0より大きい数を与えた場合は、「各々の値ごとに1行を使い、さらに指定した数の空白を使ってデータ構造をインデントする」という意味になる。
with open('basic-pretty.json', mode='w', encoding='utf-8') as f:
json.dump(basic_entry, f, indent=2)
with open('basic-pretty.json', mode='r', encoding='utf-8') as f:
print(f.read())
{ "id": 256, "published": true, "comments_link": null, "tags": [ "diveintopython", "docbook", "html" ], "title": "Dive into history, 2009 edition" }
json
はPython固有の形式ではないので、Pythonのデータ型とのミスマッチがいくつか存在する。
いくつかは単に名前が違うだけであるが、Pythonの重要なデータ型のうちの2つは完全に欠けてしまっている。
JSON | Python3 |
---|---|
オブジェクト | 辞書 |
配列 | リスト |
文字列 | 文字列 |
整数 | 整数 |
実数 | 浮動小数点数 |
true | True |
false | False |
null | None |
タプルとバイト列は対応していない。 jsonは配列型を持っており、jsonモジュールはこれをPythonのリストにマッピングする。 しかし、jsonは「凍結された配列」(タプル)を持たない。
また、jsonは、文字列を非常に上手くサポートする一方で、bytesオブジェクトやバイト配列はサポートしない。
json
が組み込みでバイト列をサポートしていないと言えども、
bytesオブジェクトのシリアライズが不可能なわけではない。
jsonモジュールは、jsonで定義されていないデータ型をエンコード/デコードするためのフックを仕掛けることができるようになっている。
jsonモジュールはもちろんバイト列の存在を認識しているが、 このモジュールはjsonの仕様によって制約されている。
バイト列やjsonがネイティブでサポートしない他のデータ型をエンコードしたい場合には、それらの型のための自作のエンコーダとデコーダを用意する必要がある。
entryデータ構造は、あらゆるものが含まれる:
entry
{'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', 'comments_link': None, 'internal_id': b'\xde\xd5\xb4\xf8', 'published': True, 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1), 'tags': ('diveintopython', 'docbook', 'html'), 'title': 'Dive into history, 2009 edition'}
jsonはテキスト形式。jsonファイルは、文字コードをutf-8にしてテキストモードで開く。
import json
with open('entry.json', 'w', encoding='utf-8') as f:
json.dump(entry, f)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-28-cf1b8398c33c> in <module>() 1 import json 2 with open('entry.json', 'w', encoding='utf-8') as f: ----> 3 json.dump(entry, f) C:\Miniconda3\lib\json\__init__.py in dump(obj, fp, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw) 176 # could accelerate with writelines in some versions of Python, at 177 # a debuggability cost --> 178 for chunk in iterable: 179 fp.write(chunk) 180 C:\Miniconda3\lib\json\encoder.py in _iterencode(o, _current_indent_level) 427 yield from _iterencode_list(o, _current_indent_level) 428 elif isinstance(o, dict): --> 429 yield from _iterencode_dict(o, _current_indent_level) 430 else: 431 if markers is not None: C:\Miniconda3\lib\json\encoder.py in _iterencode_dict(dct, _current_indent_level) 401 else: 402 chunks = _iterencode(value, _current_indent_level) --> 403 yield from chunks 404 if newline_indent is not None: 405 _current_indent_level -= 1 C:\Miniconda3\lib\json\encoder.py in _iterencode(o, _current_indent_level) 434 raise ValueError("Circular reference detected") 435 markers[markerid] = o --> 436 o = _default(o) 437 yield from _iterencode(o, _current_indent_level) 438 if markers is not None: C:\Miniconda3\lib\json\encoder.py in default(self, o) 178 179 """ --> 180 raise TypeError(repr(o) + " is not JSON serializable") 181 182 def encode(self, o): TypeError: b'\xde\xd5\xb4\xf8' is not JSON serializable
json.dump()
関数が bytesオブジェクトb'\xDE\xD5\xB4\xF8'
をシリアライズしようとしたが、jsonはbytesオブジェクトをサポートしていないので失敗した。
どうしてもバイト列を保存したい場合には、独自の「小さなシリアライズ形式」を定義することができる。
jsonがネイティブにサポートしないデータ型のための独自の「小さなシリアライズ形式」を定義するには、パラメータとして1つのPythonオブジェクトを受け取る関数を定義すればいい。
json.dump()
関数が自分自身ではシリアライズできない実際のオブジェクトが、
このPythonオブジェクトとして渡されることになる。
この例では、それはbytesオブジェクトの b'\xDE\xD5\xB4\xF8'
になる。
自作のシリアライズ関数は、json.dump()
関数が渡してきたPythonオブジェクトの型をチェックすべき。
厳密には、その関数が1つのデータ型だけをシリアライズするのであれば必要ないのだが、
このチェックは関数がどの型を対象にしているのかを非常に明白にしてくれるし、
あとになって他のデータ型をシリアライズする必要に迫られたときの拡張も容易になる。
今回はbytesオブジェクトを辞書に変換することにした。
__class__
キーが元のデータ型を(文字列'bytesとして)格納し、
__value__
キーが実際の値を格納する。
もちろん、この値としてbytesオブジェクトを使うことはできないので、これをjsonでシリアライズできる何らかの値に変換することが必要。
bytes
は整数のシーケンスなので、各々の整数は 0–255 の範囲のいずれかになる。
list()
関数を使ってbytesオブジェクトを整数のリストに変換することができる。
つまり、b'\xDE\xD5\xB4\xF8'
は [222, 213, 180, 248]
になる(計算すれば分かる
16進数で\xDEのバイトは10進数では222(16*13+14)。\xD5は213。以下同様)。
def to_json(python_object):
if isinstance(python_object, bytes):
return {'__class__': 'bytes',
'__value__': list(python_object)}
raise TypeError(repr(python_object) + ' is not JSON serializable')
最後の行は、シリアライズしようとしているデータは、
組み込みのjsonシリアライザでも、自作のシリアライザでも扱えない型を含んでいるかもしれない。
その場合は、自作のシリアライザが TypeError
を発生させることによって、
自作のシリアライザがその型を認識できないことを json.dump()
関数に知らせなければならない。
これで完了。他のことはしなくていい。
具体的に言うと、この自作のシリアライズ関数は、文字列を返さずに、Pythonの辞書を返している。
自分自身ではjsonへの完全なシリアライズを行っていない。
やっているのはサポートされているデータ型に変換する部分だけだ。
残りの処理は json.dump()
関数がやってくれる。
自作の変換用関数を json.dump()
関数にフックするために、
その関数を json.dump()
関数のdefaultパラメータで渡している。
with open('entry.json', 'w', encoding='utf-8') as f:
json.dump(entry, f, default=to_json)
with open('entry.json', 'r', encoding='utf-8') as f:
print(f.read())
{"article_link": "http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition", "published_date": [2009, 3, 27, 22, 20, 42, 4, 86, -1], "title": "Dive into history, 2009 edition", "internal_id": {"__value__": [222, 213, 180, 248], "__class__": "bytes"}, "comments_link": null, "tags": ["diveintopython", "docbook", "html"], "published": true}
time.struct_time
オブジェクトが、
配列になってしまう問題がある。
"published_date": [2009, 3, 27, 22, 20, 42, 4, 86, -1]
先ほどの to_json()
関数にコードを加え、(json.dump()
関数が扱い困っている)
Pythonオブジェクトが time.struct_time
であるかどうかをチェックする必要がある。
もしそうであれば、bytesオブジェクトに対して行った変換と似たようなことを行う:
time.struct_time
オブジェクトを、jsonがシリアライズできる値だけで構成された辞書に変換する。
この場合、日時をjsonがシリアライズできる値に最も簡単に変換するには、
time.asctime()
関数を使って文字列に変換すると良い。
time.asctime()
関数は、
見た目の悪いtime.struct_time
を 'Fri Mar 27 22:20:42 2009'
という文字列に変換する。
import time
def to_json1(python_object):
if isinstance(python_object, time.struct_time):
return {'__class__': 'time.asctime',
'__value__': time.asctime(python_object)}
if isinstance(python_object, bytes):
return {'__class__': 'bytes',
'__value__': list(python_object)}
raise TypeError(repr(python_object) + ' is not JSON serializable')
これら2つのカスタムな変換をすれば、 これ以上の問題を起こすことなくentryデータ構造の全体がjsonにシリアライズされるはず。
with open('entry.json', 'w', encoding='utf-8') as f:
json.dump(entry, f, default=to_json1)
with open('entry.json', 'r', encoding='utf-8') as f:
print(f.read())
{"article_link": "http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition", "published_date": [2009, 3, 27, 22, 20, 42, 4, 86, -1], "title": "Dive into history, 2009 edition", "internal_id": {"__value__": [222, 213, 180, 248], "__class__": "bytes"}, "comments_link": null, "tags": ["diveintopython", "docbook", "html"], "published": true}
json.dump()
の際にデフォで使われるJSONEncoder
クラスをカスタマイズすることもできる。
default
メソッドをオーバーライドすることで、変換用関数を作れる。
import json
import time
class TimeEncoder(json.JSONEncoder):
def default(self, python_object):
print(str(type(python_object)))
if isinstance(python_object, time.struct_time):
return {'__class__': 'time.asctime', '__value__': time.asctime(python_object)}
if isinstance(python_object, bytes):
return {'__class__': 'bytes', '__value__': list(python_object)}
# Let the base class default method raise the TypeError
return json.JSONEncoder.default(self, python_object)
with open('entry.json', 'w', encoding='utf-8') as f:
json.dump(entry, f, cls=TimeEncoder)
<class 'bytes'>
with open('entry.json', 'r', encoding='utf-8') as f:
print(f.read())
{"article_link": "http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition", "published_date": [2009, 3, 27, 22, 20, 42, 4, 86, -1], "title": "Dive into history, 2009 edition", "internal_id": {"__value__": [222, 213, 180, 248], "__class__": "bytes"}, "comments_link": null, "tags": ["diveintopython", "docbook", "html"], "published": true}
isinstance(entry['published_date'], time.struct_time)
True
type(entry['published_date'])
time.struct_time
どうやらtime.struct_time
は、独自オブジェクトでなく、namedtuple(名前付きタプル)になっているようで、
そのため、json.dump()
すると タプル同様、配列 に変換されてしまっている。
import json
json = json.dumps(time.strptime('Fri Mar 27 22:20:42 2009'))
json
'[2009, 3, 27, 22, 20, 42, 4, 86, -1]'
time.strptime('Fri Mar 27 22:20:42 2009')
time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1)
time.strptime('Fri Mar 27 22:20:42 2009')
type(time.strptime('Fri Mar 27 22:20:42 2009'))
time.struct_time
time.struct_time
が使いずらいので、datetime
を使ってみる。
time.strptime
の利点としては、文字列のフォーマットを指定しなくてもtime.struct_time
に変換してくれる。
import datetime
st = time.strptime('Fri Mar 27 22:20:42 2009')
dt = datetime.datetime(st.tm_year,st.tm_mon,st.tm_mday,st.tm_hour,st.tm_min,st
.tm_sec)
dt
datetime.datetime(2009, 3, 27, 22, 20, 42)
datetime.strftime()
と datetime.strptime()
のフォーマットについては以下のリンク。
http://docs.python.jp/3.5/library/datetime.html#strftime-strptime-behavior
str = dt.strftime('%Y/%m/%d')
str
'2009/03/27'
str = dt.strftime('%c')
str
'Fri Mar 27 22:20:42 2009'
import time
import datetime
entry = {}
entry['title'] = 'Dive into history, 2009 edition'
entry['article_link'] = 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'
entry['comments_link'] = None
entry['internal_id'] = b'\xDE\xD5\xB4\xF8'
entry['tags'] = ('diveintopython', 'docbook', 'html')
entry['published'] = True
time_struct = time.strptime('Fri Mar 27 22:20:42 2009')
entry['published_date'] = datetime.datetime(*time_struct[:6])
entry['published_date']
datetime.datetime(2009, 3, 27, 22, 20, 42)
import datetime
def to_json(python_object):
if isinstance(python_object, datetime.datetime):
return {'__class__': 'datetime.datetime', '__value__': python_object.strftime('%c')}
if isinstance(python_object, bytes):
return {'__class__': 'bytes', '__value__': list(python_object)}
# Let the base class default method raise the TypeError
return json.JSONEncoder.default(self, python_object)
import json
with open('entry.json', 'w', encoding='utf-8') as f:
json.dump(entry, f, default=to_json)
with open('entry.json', 'r', encoding='utf-8') as f:
print(f.read())
{"article_link": "http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition", "published_date": {"__value__": "Fri Mar 27 22:20:42 2009", "__class__": "datetime.datetime"}, "title": "Dive into history, 2009 edition", "internal_id": {"__value__": [222, 213, 180, 248], "__class__": "bytes"}, "comments_link": null, "tags": ["diveintopython", "docbook", "html"], "published": true}
json
モジュールは、pickle
モジュールのように load()
関数を持っている。
この関数は、ストリームオブジェクトを受け取り、そのオブジェクトからjsonでエンコードされたデータを読み込み、jsonのデータ構造を写しとった新しいPythonのオブジェクトを作成する。
今まで使っていた entry
データ構造を削除する。
del entry
entry
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-50-9906af9c4100> in <module>() 1 del entry ----> 2 entry NameError: name 'entry' is not defined
json.load()
関数は、 pickle.load()
関数と同様にうまく機能する。
この関数は、オブジェクトを渡すと新しいPythonオブジェクトを返す。
import json
with open('entry.json', 'r', encoding='utf-8') as f:
entry = json.load(f)
json.load()
関数は、entry.json
を正常に読み込んで、そのデータを格納する新しいPythonオブジェクトを作る。
悪い知らせ:
この関数は entry データ構造を再構成しない。2つの値'internal_id'と'published_date'は辞書だ。具体的に言うと、これらは、変換関数to_json()で作ったjson互換の値を持った辞書になっている。
entry
{'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', 'comments_link': None, 'internal_id': {'__class__': 'bytes', '__value__': [222, 213, 180, 248]}, 'published': True, 'published_date': {'__class__': 'datetime.datetime', '__value__': 'Fri Mar 27 22:20:42 2009'}, 'tags': ['diveintopython', 'docbook', 'html'], 'title': 'Dive into history, 2009 edition'}
json.load()
は、先にjson.dump()
関数に渡された変換関数については何も知らない。
to_json()
関数とは反対のことをする関数が必要になる。
つまり、カスタムな変換を施されたjsonオブジェクトを受け取って、それを元のPythonのデータ型に変換する関数が必要。
datetime.datetime.strftime関数によって返された文字列をデコードするには、time.strptime()関数('time.struct_time')を使っている。
この関数はフォーマットされた日時を表す文字列を受け取り(形式はカスタマイズできるが、
標準ではtime.asctime()がデフォルトで返す文字列と同じ形式(datetime.datetime.strptime('%c')
)をとる)time.struct_time
を返す。
整数のリストを bytes
オブジェクトに変換するには、bytes()関数を使うことができる。
def from_json(json_object):
if '__class__' in json_object:
if json_object['__class__'] == 'datetime.datetime':
return time.strptime(json_object['__value__'])
if json_object['__class__'] == 'bytes':
return bytes(json_object['__value__'])
return json_object
with open('entry.json', 'r', encoding='utf-8') as f:
entry = json.load(f, object_hook=from_json)
デシリアライズの処理の中に from_json()
関数をフックするために、
この関数を object_hook
パラメータとして、json.load()
関数に渡す。
entry
{'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', 'comments_link': None, 'internal_id': b'\xde\xd5\xb4\xf8', 'published': True, 'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1), 'tags': ['diveintopython', 'docbook', 'html'], 'title': 'Dive into history, 2009 edition'}
元のentryデータ構造では、'tags'
キーの値は3つの文字列からなるタプルだったが、
配列に変換されてしまう。
entry['tags']
['diveintopython', 'docbook', 'html']
jsonはタプルとリストを区別することができない。
jsonにはリストに似たデータ型である配列しか存在しないので、
jsonモジュールはシリアライズの過程でタプルとリストの両方を黙ってjsonの配列に変換する。
ほとんどの場合、タプルとリストの違いは無視することができるが、
json
モジュールを使うときには、このことを心に留めておいたほうが良い。
namedtuple
は、配列に変換されてしまうので注意。TypeError
も出ない。JSON | Python3 |
---|---|
オブジェクト | 辞書 |
配列 | リスト、タプル、namedtuple |
文字列 | 文字列 |
整数 | 整数 |
実数 | 浮動小数点数 |
true | True |
false | False |
null | None |
なので、 Python側で、「辞書、リスト、文字列、整数、浮動小数点数、ブール、None」 のみのデータ構造であれば、 処理を追加せずともjsonとPythonオブジェクト間で整合性がとれる。