現在地: ホーム ‣ Dive Into Python 3 ‣
難易度: ♦♦♦♦♢
❝このアパートに来てから毎週土曜は、6時15分に起きて、低脂肪牛乳を1/4カップ入れたシリアルを食べながら、
このカウチの、この端に座って、BBCアメリカでドクター・フーを観てるんだよ。❞
— Sheldon, The Big Bang Theory
シリアライズの概念は、表面上はシンプルだ。保存したり、再利用したり、他の人に送信したいデータをメモリ上に持っているとしよう。あなたならそれをどのように行うだろうか? もちろんこれは、そのデータをどのように保存したいのか、どのように再利用したいのか、だれに送りたいのかによって違うだろう。多くのゲームは終了時に「セーブ」を行い、ゲームを再び起動したときに以前の場所から再開できるようになっている(実際には、ゲーム以外の多くのアプリケーションも似たようなことを行っている)。この場合、ゲームの終了時にどこまで進んだかを記録するデータ構造がディスクに保存され、再び起動するときにディスクから読み込まれる必要がある。このデータは、作成したのと同じアプリケーションによって読み込まれるだけで、ネットワーク越しに転送されたり、作成したアプリケーション以外によって読み込まれることは無いという想定で作られている。したがって、相互運用性に関して考慮すべきことは、古いバージョンのプログラムが作成したデータを、新しいバージョンのプログラムが読み込めることを保証することに限られる。
このような場合にはpickle
モジュールがうってつけだ。これはPython標準ライブラリに含まれているのでいつでも利用できるし、Pythonインタプリタと同様に大部分がC言語で書かれているので処理も速い。そして、どんなに複雑なPythonのデータ構造でも保存することができる。
pickle
モジュールは何を保存できるのか?
bytes
オブジェクト・バイト配列・None
。
これでも不十分な場合は、pickle
モジュールを拡張することもできる。この拡張性に興味がある人は、この章の最後にあるもっと知りたい人のためにのリンクを確認してほしい。
この章では、2つのPythonシェルを使って話をする。この章では、すべての例示によって1つの話が構成されている。私がpickle
とjson
モジュールの実演を行うあいだ、皆さんには2つのPythonシェルを行ったり来たりしてもらうことになる。
混乱を防ぐために、Pythonシェルを開いて次の変数を定義しよう。
>>> shell = 1
このウインドウは開いたままにしておこう。今度は、もう一つの新しいPythonシェルを開いて次の変数を定義しよう。
>>> shell = 2
この章では、各々の例でどちらのPythonシェルが使われているのかをshell
変数を使って示すことにする。
⁂
pickle
モジュールはデータ構造を扱う。まずは1つ作ってみよう。
>>> shell ① 1 >>> 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 >>> 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)
pickle
モジュールの能力を見せびらかすために、この辞書が様々なデータ型を格納していることも強調しておきたい。これらの値の内容については気にしなくて良い。
time
モジュールは、時刻上の1点(ミリ秒までの精度を持つ)を表現するためのデータ構造 (time_struct
) と、そのデータ構造を操作するための関数を含んでいる。strptime()
関数は、フォーマットされた文字列を受け取ってtime_struct
に変換する。この文字列はデフォルトの形式で表現されているが、フォーマットコードによって制御することもできる。詳しくはtime
モジュールを見てほしい。
カッコイイPythonの辞書ができた。これをファイルに保存しよう。
>>> shell ① 1 >>> import pickle >>> with open('entry.pickle', 'wb') as f: ② ... pickle.dump(entry, f) ③ ...
open()
関数を使ってファイルを開く。ファイルをバイナリモードで開くために、ファイルモードは'wb'
に設定しよう。これをwith
文で包むことで、保存が終わったときに自動的にファイルが閉じられるようにしておく。
pickle
モジュールのdump()
関数は、シリアライズ可能なPythonのデータ構造を1つ受け取り、それを最新のバージョンのPickleプロトコルを用いてPython固有のバイナリ形式にシリアライズした上で、開いておいたファイルにそれを保存する。
最後の文はとても重要だ。
pickle
モジュールはPythonのデータ構造を受け取り、それをファイルに保存する。
entry.pickle
ファイルを有効に使うことはできないだろう。
pickle
モジュールは、Pythonのあらゆるデータ構造をシリアライズできるわけではない。Pythonに新しい型が追加されるたびにPickleプロトコルには何度も変更が加えられてきているが、制限は依然として存在する。
pickle
モジュールの関数は、最新バージョンのPickleプロトコルを使用する。このおかげでシリアライズできるデータ型の範囲は最も広くなるのだが、その代償として、最新のPickleプロトコルをサポートしていない古いバージョンのPythonでは生成されるPickleファイルを読み込めなくなってしまう。
⁂
今度は2つ目のPythonシェル — つまりentry
辞書を作ったシェルではない方のシェルに切り替えよう。
>>> shell ① 2 >>> entry ② Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'entry' is not defined >>> import pickle >>> with open('entry.pickle', 'rb') as f: ③ ... entry = pickle.load(f) ④ ... >>> entry ⑤ {'comments_link': None, 'internal_id': b'\xDE\xD5\xB4\xF8', 'title': 'Dive into history, 2009 edition', 'tags': ('diveintopython', 'docbook', 'html'), 'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', '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), 'published': True}
entry.pickle
ファイルを開こう。pickle
モジュールはバイナリ形式を使うので、Pickleファイルは常にバイナリモードで開かなければならない。
pickle.load()
関数は、ストリームオブジェクトを受け取り、ストリームからシリアライズされたデータを読み込み、新しいPythonオブジェクトを作り、シリアライズされたデータを新しいPythonオブジェクトの中に再構成し、その新しいPythonオブジェクトを返す。
pickle.dump() / pickle.load()
のサイクルによって、元のデータ構造と等しい内容をもつ新たなデータ構造がもたらされる。
>>> shell ① 1 >>> with open('entry.pickle', 'rb') as f: ② ... entry2 = pickle.load(f) ③ ... >>> entry2 == entry ④ True >>> entry2 is entry ⑤ False >>> entry2['tags'] ⑥ ('diveintopython', 'docbook', 'html') >>> entry2['internal_id'] b'\xDE\xD5\xB4\xF8'
entry.pickle
ファイルを開こう。
entry.pickle
ファイルに保存した。さらに、そのファイルからシリアライズされたデータを読み込んで、元のデータ構造の完全な複製を作成した。
'tags'
キーの値がタプルであることと、'internal_id'
キーの値がbytes
オブジェクトであることを指摘しておきたい。指摘した理由は後で分かる。
⁂
前節の例は、Pythonオブジェクトをディスク上のファイルに直接シリアライズする方法を示していた。しかし、ファイルが必要ない場合や、ファイルを使わずにやりたい場合はどうすればいいのだろうか? pickle
は、メモリ上のbytes
オブジェクトにシリアライズすることもできるのだ。
>>> shell 1 >>> b = pickle.dumps(entry) ① >>> type(b) ② <class 'bytes'> >>> entry3 = pickle.loads(b) ③ >>> entry3 == entry ④ True
pickle.dumps()
関数(関数名の末尾に's'
が付いていることに注意)は、pickle.dump()
関数と同様のシリアライズを行う。ただし、ストリームオブジェクトを受け取って、ディスク上のファイルにシリアライズしたデータを書き込む代わりに、ただ単にシリアライズしたデータを返すだけだ。
pickle.dumps()
関数はbytes
オブジェクトを返す。
pickle.loads()
関数(再び、関数名の末尾に's'
が付いていることに注意)は、pickle.load()
関数と同様のデシリアライズを行う。ただし、ストリームオブジェクトを受け取って、ファイルからシリアライズされたデータを読み込む代わりに、pickle.dumps()
関数が生成するような、シリアライズされたデータを含んだbytes
オブジェクトを受け取る。
⁂
Pickleプロトコルが最初に考案されたのは何年も前のことであり、Pythonが言語として成熟するのにあわせて、このプロトコルも成長してきた。現在は4つの異なるバージョンのPickleプロトコルが存在する。
bytes
オブジェクトとバイト配列の明示的なサポートをもつ、もう一つのPickleプロトコルが導入された(バージョン3)。これはバイナリ形式だ。
ああ、見てほしい。バイト列と文字列の違いが再び不快な姿を表した(ここで驚いたのなら、注意を払っていなかったのだろう)。これが実際問題として示す意味はこうだ。つまり、Python 3はプロトコルバージョン2でPickle化されたデータを読み込むことができるが、Python 2はプロトコルバージョン3でPickle化されたデータを読み込むことができないのだ。
⁂
Pickleプロトコルはどのような姿をしているのだろうか? Pythonシェルから少しのあいだ飛び出して、さきほど作ったentry.pickle
を見てみよう。
you@localhost:~/diveintopython3/examples$ ls -l entry.pickle -rw-r--r-- 1 you you 358 Aug 3 13:34 entry.pickle you@localhost:~/diveintopython3/examples$ cat entry.pickle comments_linkqNXtagsqXdiveintopythonqXdocbookqXhtmlq?qX publishedq? XlinkXJhttp://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition q Xpublished_dateq ctime struct_time ?qRqXtitleqXDive into history, 2009 editionqu.
これでは何の参考にもならない。文字列は判読できるものの、他のデータ型は非印字文字(もしくは、少なくとも読めない文字)になってしまっているし、フィールドがタブやスペースで区切られているわけでもないようだ。この形式を自分でデバッグしようとは思わないだろう。
>>> shell 1 >>> 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 'published_date' 25: q BINPUT 1 27: c GLOBAL 'time struct_time' 45: q BINPUT 2 47: ( MARK 48: M BININT2 2009 51: K BININT1 3 53: K BININT1 27 55: K BININT1 22 57: K BININT1 20 59: K BININT1 42 61: K BININT1 4 63: K BININT1 86 65: J BININT -1 70: t TUPLE (MARK at 47) 71: q BINPUT 3 73: } EMPTY_DICT 74: q BINPUT 4 76: \x86 TUPLE2 77: q BINPUT 5 79: R REDUCE 80: q BINPUT 6 82: X BINUNICODE 'comments_link' 100: q BINPUT 7 102: N NONE 103: X BINUNICODE 'internal_id' 119: q BINPUT 8 121: C SHORT_BINBYTES 'ÞÕ´ø' 127: q BINPUT 9 129: X BINUNICODE 'tags' 138: q BINPUT 10 140: X BINUNICODE 'diveintopython' 159: q BINPUT 11 161: X BINUNICODE 'docbook' 173: q BINPUT 12 175: X BINUNICODE 'html' 184: q BINPUT 13 186: \x87 TUPLE3 187: q BINPUT 14 189: X BINUNICODE 'title' 199: q BINPUT 15 201: X BINUNICODE 'Dive into history, 2009 edition' 237: q BINPUT 16 239: X BINUNICODE 'article_link' 256: q BINPUT 17 258: X BINUNICODE 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition' 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
これはファイルを逆アセンブルしたものだが、最も興味深い情報は最後の行に含まれている。というのも、この行にはファイルを保存したときに使ったPickleプロトコルのバージョンが記されているからだ。Pickleプロトコルには、バージョンを示すための明示的なマーカが存在しない。したがって、ファイルの保存にどのバージョンのプロトコルが用いられたのかを知るためには、Pickle化されたデータの中にある目印(“opcodes”)に目を向けて、「どのopcodeがどのバージョンのPickleプロトコルで導入されたのか」というハードコードされた知識をもとに推測を行うしかない。pickletools.dis()
関数もこの方法でバージョンを識別しており、その結果を逆アセンブリ出力の最後の行に表示してくれる。次に示すのは、何も表示することなく、単にバージョン番号のみを返す関数だ。
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
これは次のように使う:
>>> import pickleversion >>> with open('entry.pickle', 'rb') as f: ... v = pickleversion.protocol_version(f) >>> v 3
⁂
pickle
モジュールが使っているデータ形式はPython固有のものであり、他のプログラミング言語との互換性をとる努力は行われていない。言語間の互換性が必要な場合は、他のシリアライズ形式に目を向ける必要がある。そのような形式の1つとしてJSONがある。“JSON” は “JavaScript Object Notation” の略だが、この名前に騙されてはいけない。JSONは、様々な言語間で使えるように明確に設計されているのだ。
Python 3の標準ライブラリの中にはjson
モジュールが含まれている。json
モジュールは、pickle
モジュールと同様に、データ構造をシリアライズする関数、シリアライズされたデータをディスクに保存する関数、シリアライズされたデータをディスクから読み込む関数、データをデシリアライズして新しいPythonオブジェクトに戻す関数を持っている。しかし、重要な違いもいくつか存在する。まず第一に、JSONのデータ形式はバイナリ形式ではなくテキスト形式だ。RFC 4627は、JSONの形式と、個々の異なる型がどのようにテキストとしてエンコードされるのかを定義している。例えばブール値は、5文字の文字列'false'
、もしくは4文字の文字列'true'
として保存される。JSONのすべての値は、大文字と小文字が区別される。
第二に、テキストベースの形式ではいつも問題となることだが、ホワイトスペースの扱いがある。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()
関数を使って「デコード」することができる(通例の信頼されていない入力に関する警告は適用されるが、ここで言いたいのは、JSONはJavaScriptの値として有効だということだ)。そんなわけで、JSON はあなたにとって見覚えのあるものかもしれない。
>>> shell 1 >>> 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 >>> import json >>> with open('basic.json', mode='w', encoding='utf-8') as f: ② ... json.dump(basic_entry, f) ③
pickle
モジュールと同様に、json
モジュールは、Pythonのデータ構造と書き込み可能なストリームオブジェクトを受け取るdump()
関数を定義している。dump()
関数はPythonのデータ構造をシリアライズして、それをストリームオブジェクトに書き込む。これをwith
文の中で行うことによって、処理が終わったときにファイルが適切に閉じられることを保証している。
では、JSONのシリアライズ結果はどうなっているだろうか?
you@localhost:~/diveintopython3/examples$ cat basic.json {"published": true, "tags": ["diveintopython", "docbook", "html"], "comments_link": null, "id": 256, "title": "Dive into history, 2009 edition"}
これは明らかにPickleファイルよりも読みやすい。しかし、JSONは、値のあいだに任意個のホワイトスペースを入れることができるので、json
モジュールはこの長所を生かして、さらに読みやすいJSONファイルを手軽に作る方法を提供している。
>>> shell
1
>>> with open('basic-pretty.json', mode='w', encoding='utf-8') as f:
... json.dump(basic_entry, f, indent=2) ①
json.dump()
関数に渡すと、JSONファイルをより読みやすい形に整形して出力してくれる(その代わりに、ファイルサイズは増えるが)。indentパラメータは整数だ。これを0にすると「各々の値ごとに1行を使う」という意味になる。0より大きい数を与えた場合は、「各々の値ごとに1行を使い、さらに指定した数の空白を使ってデータ構造をインデントする」という意味になる。
そして、これがその結果だ:
you@localhost:~/diveintopython3/examples$ cat basic-pretty.json { "published": true, "tags": [ "diveintopython", "docbook", "html" ], "comments_link": null, "id": 256, "title": "Dive into history, 2009 edition" }
⁂
JSONはPython固有の形式ではないので、Pythonのデータ型とのミスマッチがいくつか存在する。いくつかは単に名前が違うだけであるが、Pythonの重要なデータ型のうちの2つは完全に欠けてしまっている。その2つを以下の表から見つけることができるだろうか:
注 | JSON | Python 3 |
---|---|---|
オブジェクト | 辞書 | |
配列 | リスト | |
文字列 | 文字列 | |
整数 | 整数 | |
実数 | 浮動小数点数 | |
* | true
| True
|
* | false
| False
|
* | null
| None
|
* すべてのJSONの値は大文字と小文字が区別される。 |
何が欠けているか気付いただろうか? タプルとバイト列だ。JSONは配列型を持っており、json
モジュールはこれをPythonのリストにマッピングする。しかし、JSONは「凍結された配列」(タプル)を持たない。また、JSONは、文字列を非常に上手くサポートする一方で、bytes
オブジェクトやバイト配列はサポートしない。
⁂
JSONが組み込みでバイト列をサポートしていないと言えども、bytes
オブジェクトのシリアライズが不可能なわけではない。json
モジュールは、未知のデータ型をエンコード/デコードするためのフックを仕掛けることができるようになっている(「未知の」という言葉は「JSONで定義されていない」という意味だ。json
モジュールはもちろんバイト列の存在を認識しているが、このモジュールはJSONの仕様によって制約されているのだ)。バイト列やJSONがネイティブでサポートしない他のデータ型をエンコードしたい場合には、それらの型のための自作のエンコーダとデコーダを用意する必要がある。
>>> shell 1 >>> entry ① {'comments_link': None, 'internal_id': b'\xDE\xD5\xB4\xF8', 'title': 'Dive into history, 2009 edition', 'tags': ('diveintopython', 'docbook', 'html'), 'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', '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), 'published': True} >>> import json >>> with open('entry.json', 'w', encoding='utf-8') as f: ② ... json.dump(entry, f) ③ ... Traceback (most recent call last): File "<stdin>", line 5, in <module> File "C:\Python31\lib\json\__init__.py", line 178, in dump for chunk in iterable: File "C:\Python31\lib\json\encoder.py", line 408, in _iterencode for chunk in _iterencode_dict(o, _current_indent_level): File "C:\Python31\lib\json\encoder.py", line 382, in _iterencode_dict for chunk in chunks: File "C:\Python31\lib\json\encoder.py", line 416, in _iterencode o = _default(o) File "C:\Python31\lib\json\encoder.py", line 170, in default raise TypeError(repr(o) + " is not JSON serializable") TypeError: b'\xDE\xD5\xB4\xF8' is not JSON serializable
None
、文字列、文字列のタプル、bytes
オブジェクト、そしてtime
データ構造だ。
ここで起きたのは次のようなことだ: json.dump()
関数がbytes
オブジェクトb'\xDE\xD5\xB4\xF8'
をシリアライズしようとしたが、JSONはbytes
オブジェクトをサポートしていないので失敗した。しかし、どうしてもバイト列を保存したい場合には、独自の「小さなシリアライズ形式」を定義することができる。
[customserializer.py
をダウンロードする]
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.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。\xD5
は213。以下同様)。
TypeError
を発生させることによって、自作のシリアライザがその型を認識できないことをjson.dump()
関数に知らせなければならない。
これですべてだ。他のことはしなくていい。具体的に言うと、この自作のシリアライズ関数は、文字列を返さずに、Pythonの辞書を返している。自分自身ではJSONへの完全なシリアライズを行っていない。やっているのはサポートされているデータ型に変換する部分だけだ。残りの仕事はjson.dump()
関数がやってくれる。
>>> shell 1 >>> import customserializer ① >>> with open('entry.json', 'w', encoding='utf-8') as f: ② ... json.dump(entry, f, default=customserializer.to_json) ③ ... Traceback (most recent call last): File "<stdin>", line 9, in <module> json.dump(entry, f, default=customserializer.to_json) File "C:\Python31\lib\json\__init__.py", line 178, in dump for chunk in iterable: File "C:\Python31\lib\json\encoder.py", line 408, in _iterencode for chunk in _iterencode_dict(o, _current_indent_level): File "C:\Python31\lib\json\encoder.py", line 382, in _iterencode_dict for chunk in chunks: File "C:\Python31\lib\json\encoder.py", line 416, in _iterencode o = _default(o) File "/Users/pilgrim/diveintopython3/examples/customserializer.py", line 12, in to_json raise TypeError(repr(python_object) + ' is not JSON serializable') ④ TypeError: 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) is not JSON serializable
customserializer
モジュールは、前節でto_json()
関数を定義したファイルだ。
json.dump()
関数にフックするために、その関数をjson.dump()
関数のdefaultパラメータで渡している(やったね、Pythonではあらゆるものがオブジェクトだ!)。
json.dump()
関数は、bytes
オブジェクトをシリアライズできない、という文句を言っていない。今度は、違うオブジェクトについて文句を言っている。time.struct_time
オブジェクトだ。
別の例外が発生したので、前進したようには見えないかもしれないが、ちゃんと解決には近づいている! あともう一手間加えれば、この問題を乗り越えることができる。
import time
def to_json(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')
customserializer.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'
という文字列に変換する。
これら2つのカスタムな変換をすれば、これ以上の問題を起こすことなくentryデータ構造の全体がJSONにシリアライズされるはずだ。
>>> shell 1 >>> with open('entry.json', 'w', encoding='utf-8') as f: ... json.dump(entry, f, default=customserializer.to_json) ...
you@localhost:~/diveintopython3/examples$ ls -l example.json -rw-r--r-- 1 you you 391 Aug 3 13:34 entry.json you@localhost:~/diveintopython3/examples$ cat example.json {"published_date": {"__class__": "time.asctime", "__value__": "Fri Mar 27 22:20:42 2009"}, "comments_link": null, "internal_id": {"__class__": "bytes", "__value__": [222, 213, 180, 248]}, "tags": ["diveintopython", "docbook", "html"], "title": "Dive into history, 2009 edition", "article_link": "http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition", "published": true}
⁂
json
モジュールは、pickle
モジュールのようにload()
関数を持っている。この関数は、ストリームオブジェクトを受け取り、そのオブジェクトからJSONでエンコードされたデータを読み込み、JSONのデータ構造を写しとった新しいPythonのオブジェクトを作成する。
>>> shell 2 >>> del entry ① >>> entry Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'entry' is not defined >>> import json >>> with open('entry.json', 'r', encoding='utf-8') as f: ... entry = json.load(f) ② ... >>> entry ③ {'comments_link': None, 'internal_id': {'__class__': 'bytes', '__value__': [222, 213, 180, 248]}, 'title': 'Dive into history, 2009 edition', 'tags': ['diveintopython', 'docbook', 'html'], 'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', 'published_date': {'__class__': 'time.asctime', '__value__': 'Fri Mar 27 22:20:42 2009'}, 'published': True}
pickle
モジュールを使って定義した entry データ構造を削除する。
json.load()
関数はpickle.load()
関数と同様にうまく機能する。この関数は、オブジェクトを渡すと新しいPythonオブジェクトを返す。
json.load()
関数は、Pythonシェル #1 で作成したentry.json
を正常に読み込んで、そのデータを格納する新しいPythonオブジェクトを作る。次に悪い知らせ: この関数は entry データ構造を再構成しない。2つの値'internal_id'
と'published_date'
は辞書だ。具体的に言うと、これらは、変換関数to_json()
で作ったJSON互換の値を持った辞書になっている。
json.load()
は、先にjson.dump()
関数に渡された変換関数については何も知らない。ここで必要なのはto_json()
関数とは反対のことをする関数だ。つまり、カスタムな変換を施されたJSONオブジェクトを受け取って、それを元のPythonのデータ型に変換する関数だ。
# add this to customserializer.py
def from_json(json_object): ①
if '__class__' in json_object: ②
if json_object['__class__'] == 'time.asctime':
return time.strptime(json_object['__value__']) ③
if json_object['__class__'] == 'bytes':
return bytes(json_object['__value__']) ④
return json_object
to_json()
によって作られた'__class__'
キーを持っているかどうかをチェックすることだけだ。もし、オブジェクトがこのキーを持っているのであれば、元のPythonのデータ型に値をデコードする方法を'__class__'
キーが教えてくれる。
time.asctime()
関数によって返された文字列をデコードするには、time.strptime()
関数を使えばよい。この関数はフォーマットされた日時を表す文字列を受け取り(形式はカスタマイズできるが、標準ではtime.asctime()
がデフォルトで返す文字列と同じ形式をとる)time.struct_time
を返す。
bytes
オブジェクトに変換するには、bytes()
関数を使うことができる。
これだけだ。to_json()
関数で特別に処理したデータ型は2つだけで、これら2つのデータ型はfrom_json()
関数でも処理されるようになった。以下がその結果だ:
>>> shell 2 >>> import customserializer >>> with open('entry.json', 'r', encoding='utf-8') as f: ... entry = json.load(f, object_hook=customserializer.from_json) ① ... >>> entry ② {'comments_link': None, 'internal_id': b'\xDE\xD5\xB4\xF8', 'title': 'Dive into history, 2009 edition', 'tags': ['diveintopython', 'docbook', 'html'], 'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', '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), 'published': True}
from_json()
関数をフックするために、この関数をobject_hookパラメータとしてjson.load()
関数に渡す。関数を受け取る関数というのはとても便利だね!
'internal_id'
を持ち、その値はbytes
オブジェクトだ。キー'published_date'
も持っており、その値はtime.struct_time
オブジェクトだ。
しかし、問題があと1つ存在する。
>>> shell 1 >>> import customserializer >>> with open('entry.json', 'r', encoding='utf-8') as f: ... entry2 = json.load(f, object_hook=customserializer.from_json) ... >>> entry2 == entry ① False >>> entry['tags'] ② ('diveintopython', 'docbook', 'html') >>> entry2['tags'] ③ ['diveintopython', 'docbook', 'html']
to_json()
関数をフックし、デシリアライズ処理にfrom_json()
関数をフックし終えたにもかかわらず、私たちは元のデータの完全な複製を再構成することができない。なぜだろう?
'tags'
キーの値は3つの文字列からなるタプルだった。
'tags'
キーの値は3つの文字列からなるリストになっている。JSONはタプルとリストを区別することができない。JSONにはリストに似たデータ型である配列しか存在しないので、json
モジュールはシリアライズの過程でタプルとリストの両方を黙ってJSONの配列に変換する。ほとんどの場合、タプルとリストの違いは無視することができるが、json
モジュールを使うときには、このことを心に留めておいたほうが良い。
☞
pickle
モジュールに関する多くの記事はcPickle
について言及している。Python 2にはpickle
モジュールの実装が2つあった。1つは純粋にPythonだけで書かれたもので、もう片方はC言語で書かれたものだ(でもPythonから呼び出せる)。Python 3では、これらの2つのモジュールは統合されたので、常にimport pickle
としてインポートしなければならない。これらの記事が役に立つこともあるだろうけれど、cPickle
についての時代遅れな情報は無視しよう。
pickle
モジュールを使ったPickle化について:
pickle
module
pickle
andcPickle
— Python object serialization
pickle
JSONとjson
モジュールについて:
json
— JavaScript Object Notation Serializer
Pickleの拡張性について:
© 2001– Mark Pilgrim
© Fukada, Fujimoto(日本語版)