このブログを検索

第13章 Pythonオブジェクトのシリアル化

難易度: ♦♦♦♦♢

13.1. 没頭しよう

一見すると、シリアル化の考え方はシンプルなものです。 メモリ上にデータストラクチャがあって、保存、再利用、または他の人に送りたいとき、どうしますか?その答えは、どのように保存したいのか、どのように再利用したいのか 、誰に送りたいのか、ということによって変わってきます。多くのゲームでは、中断するときには、進んだところまでを保存して、再開するときに中断箇所から始められます(これは、ゲームではないアプリの多くでも同じことです)。
この場合は、ゲームを止めるときに"ここまでの進捗"を保持しているデータストラクチャがディスクに保存され、再開するときにディスクからロードされます。データは、そのデータが作られたプログラムの中でだけ使われることを想定していて、ネットワークで送信されたり、他のプログラムで読み込まれることは全く想定していません。そのため相互運用性の問題は限定的で、前のバージョンが書き込んだデータを、後のバージョンのプログラムが確実に読み込めればよいのです。

このような場合には、pickleモジュールが最適です。
Pythonの標準ライブラリの一つなので、常に利用できます。Pythonインタプリタのように大部分がC言語で書かれていて、高速です。任意のPythonデータストラクチャを保存することができます。

Pickeモジュールは何を保存できるのでしょうか?
・Pythonがサポートするすべてのネイティブデータ型を保存できます。プーリアン、整数、浮動小数点数、複素数、文字列、バイトオブジェクト、バイトアレイ、None
・ネイティブデータ型の任意の組合せを含んだリスト、タプル、辞書、セット
・リスト、タプル、辞書、セットのリスト、タプル、辞書、セット、、(というように、Pythonの最大のネストレベルまで)
・関数、クラス、クラスのインスタンス(警告も含めて)

これでは不十分ということであれば、pickleモジュールを拡張できます。
拡張に関して知りたいときは、章末にあるFurther Readingセクションのリンクを調べてみてください。

13.1.1. この章の例に関するクイックノート

この章では、2つのPythonシェルを使って話を進めていきます。この章にあるすべての例は、どちらかのシェルでの話です。
pickleとjsonモジュールでデモをする間、2つのPythonシェルを行ったり来たりすることになります。
わかりやすくするために、Pythonシェルの中で、変数を定義しましょう。
>>> shell = 1

ウィンドウは開けたまま、もう1つ新しくPythonシェルを開いて同じ変数を定義しましょう。
>>> shell = 2

この章の中では、例の中でどちらのPythonシェルを使っているかを、変数shellを使って判別します。

13.2. Pickleファイルにデータを保存する

pickleモジュールはデータストラクチャを使います。作ってみましょう。
>>> 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)

① これはPythonシェル#1です。
# ② ここでのアイデアは、Atomフィードのエントリのようなものを持った、役に立つPython辞書を作ることです。pickleモジュールの良さをアピールするために、データの型がいくつか入っていることを強調しておきます。値に関しては深い意味はありません。
# ③ timeモジュールには、時間を表すデータストラクチャと(struct_time、精度は1ミリ秒)、それらを操作する関数があります。関数strptime()は、フォーマットされた文字列を受け取ってstruct_timeに変換します。この文字列はデフォルトのフォーマットになっていますが、フォーマットコードを使って設定できます。詳細はtimeモジュール(Pythonドキュメント)を見てください。

美しいPython辞書ができました。これをファイルに保存しましょう。
>>> shell                                    # ①
1
>>> import pickle
>>> with open('entry.pickle', 'wb') as f:    # ②
...     pickle.dump(entry, f)                # ③
...

① Pythonシェル#1にいることがわかります。
②open()関数を使ってファイルを開きます。with文を使うことで、終わったら確実に自動でファイルを閉じることができます。
③pickleモジュールにあるdump()関数は、シリアル化したPythonデータ構造を受け取り、最新のpickleプロトコルを使ってPython特有のフォーマットでバイナリ化し、開いているファイルに保存します。

この最後の一文は極めて重要でした。
・PickleモジュールはPythonデータ構造を受け取ってファイルに保存します。
・このために"pickleプロトコル"を使ってデータ構造をシリアル化します。
・PickleプロトコルはPython特有のもので、他の言語での互換性は保証されません。今作ったentry.pickleファイルをPerl、PHP、Java他の言語で何かに使うことはできません。
・Pythonデータ構造であればどんなものでもpickleモジュールでシリアル化できる、というわけではありません。Pickleプロトコルは新しいデータ型がPythonに加わると何度も変更されていますが、それでも制限はあります。
・このような変更で、Pythonのバージョン間でも互換性が保証されなくなりました。新しいバージョンのPythonでは古いシリアル化フォーマットをサポートしませんし、古いバージョンのPythonでは新しいフォーマットをサポートしません(新しいデータ型をサポートしないため)。
・指定しない限りは、pickleモジュールの関数は新しいバージョンのpickleプロトコルを使います。これによって、使えるデータ型に関しては最も汎用性があることになります。一方、出力されるファイルは、最新のpickleプロトコルをサポートしていない旧バージョンのPythonでは読めなくなります。
・最新バージョンのpickleプロトコルはバイナリフォーマットです。Pickleファイルを開くときは、必ず『バイナリモードで』にしてください。そうしないと、書き込み中にデータが壊れてしまいます。

13.3. Pickleファイルからデータをロードする

それでは2つ目のPythonシェルに切り替えましょう。さきほど辞書entryを作ったシェルではない方です。
>>> shell                                    # ①
2
>>> entry                                    # ②
Traceback (most recent call last):
  File "", line 1, in 
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}

①これはPythonシェル#2だとわかります。
②このシェルでは変数entryは定義されていません。Pythonシェル#1ではentryを定義しましたが、今回は別の状態をもった全く違う環境であることがわかります。
③Pythonシェル#1で作ったentry.pickleファイルを開きます。pickleモジュールはバイナリデータフォーマットを使うので、pickleファイルは必ずバイナリモードで開きましょう。
# ④ pickle.load()関数はストリームオブジェクトを受け取ってストリームからシリアル化されたデータを読み、新しいPythonオブジェクトを作ります。その新しいPythonオブジェクトの中にシリアル化したデータを再度作成してから、新しいPythonオブジェクトを返します。
⑤ こうして、変数entryは見たことのあるキーと値の辞書になりました。

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' 

① Pythonシェル#1に戻ります。

② entry.pickleファイルを開きます。

③ 新しい変数entry2にシリアル化したデータをロードします。

# ④ 2つの辞書entryとentry2が等しいことをPythonで確認します。このシェルでは、entryを空の辞書として作ってスタートして、手動でキーを指定して値を入れていきました。その後、辞書をシリアル化してentry.pickleファイルに保存しました。シリアル化したデータをファイルから読んで、元のデータの完全なレプリカを作ったのです。

⑤等価であるということは、同一ということではありません。先ほど書いたように、元のデータストラクチャの『完全なレプリカ』を生成したというのは本当です。ただし、それはコピーに過ぎないのです。

⑥後で明らかになりますが、'tags'キーの値はタプルで、'internal_id'キーの値はバイトオブジェクトです。

13.4. ファイルなしでPickleを使う

前のセクションでの例は、Pythonオブジェクトを直接シリアル化してディスク中のファイルに変換する方法でした。一方、ファイルを作りたくない、あるいはファイルが必要でない場合はどうしたらよいでしょうか。メモリ中のバイトオブジェクトとしてシリアル化することもできるのです。
>>> shell
1
>>> b = pickle.dumps(entry)     # ①
>>> type(b)                     # ②

>>> entry3 = pickle.loads(b)    # ③
>>> entry3 == entry             # ④
True

# ① pickle.dumps()関数は(関数名がsで終わることに注意)pickle.dump()関数と同じようにシリアル化を行います。ただし、ストリームオブジェクトを受け取ってシリアル化されたデータをディスク中のファイルにを書き込むのではなく、単にシリアル化されたデータを返します。

# ② pickleプロトコルはバイナリーデータフォーマットを使うため、pickle.dumps()関数はバイトオブジェクトを返します。

③ pickle.loads()関数は(ここでも、関数名がsで終わることに注意) pickle.load()関数と同じ逆シリアル化を実行します。ストリームオブジェクトを受け取ってファイルからシリアル化データを読むのではなく、pickle.dumps()関数から返るシリアル化データのようなバイトオブジェクトを受け取ります。

④ 最終的な結果は同じで、元の辞書の完全なレプリカとなります。

13.5. バイトと文字列が再び問題になる

pickleプロトコルは何年も使われてきて、Pythonと共に成熟してきました。現在は、4種類のpickleプロトコルがあります。

・Python1.xには2つのpickleプロトコルがありました。テキストベースフォーマット("version 0")とバイナリフォーマット("version 1")です。
・Python 2.3では新しいpick│eプロトコル("version2")が導入されました。Pythonのクラスオブジェクトの新しい機能を扱うためです。これはバイナリフォ一マットです。
・Python 3.0ではもう1つのpickleプロトコル("version3")が導入され、明示的にバイトオブジェクトとバイトアレイがサポートされるようになりました。これはバイナリフォーマットです。

気が付きましたか?バイトと文字列の違いがまた顕在化しています(驚きがなかったのであれば、注意していなかったということです)。
実用上はどうなるかと言うと、Python3はpickleプロトコルversion2によるデータを読むことはできますが、Python2ではpickleプロトコルversion3によるデータを読むことはできません。

13.6. 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ファイルを保存するときに使われたプロトコルバージョンを知るためには、pickleされたデータのマーカー("opcodes")を見たり、どのopcodesがどの
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


13.7.他の言語で読めるようにPythonオブジェクトをシリアル化する

pickleモジュールで使うデータフォーマットはPython特有のものであり、他のプログラミング言語と互換性はありません。もし言語間の互換性が必要なのであれば、他のシリアル化フォーマットを探すことになります。例えばjSONです。JSONは"JavaScript Object Notation"の略なのでこの名前に騙されそうになりますが、複数のプログラミング言語に渡って使えるような設計がされています。

Python3の標準ライブラリにはjsonモジュールが含まれています。pickleモジュールと同様に、jsonモジュールにはデータストラクチャをシリアル化する関数、シリアル化データをディスクに保存する関数、ディスクからシリアル化データをロードする関数、データを逆シリアル化して新しくPythonオブジェクトにする関数があります。一方で、大きな違いもいくつかあります。まず第1に、jsonデータフォーマットはテキストベースであって、バイナリではありません。jsonのフォーマットや、どのような種類のデータがテキストとしてエンコードされるかはRFC4627で規定されています。例えば、ブール値は5文字の'false'、または4文字の'true'として保存されます。すべてのjsonの値は大文字小文字を区別します。

第2に、テキストベースフォーマットで起こるような、空白スペースの問題です。JSONでは任意の数の空白(スペース、タブ、改行¥n¥r)を値の間に入れることができます。この空白は"取るに足らない"ものとして、jsonエンコーダは自由に空白を増やすことができ、jsonデコーダは値の間の空白を無視できるようになっています。これによってjsonデータは"pretty print"で出力され見やすいようにレベルごとに値がネストされるので、標準ブラウザやテキストエディタで読めるようになります。Pythonのjsonモジュールにはエンコーディングでのpretty-printingオプションがあります。

第3の問題は、文字エンコーディングにおける長年の問題です。JSONエンコードの値は平文ですが、以前でてきたように、"平文"というものは存在しません(4章参照)。JSONはUnicodeエンコーディングされて保存されます(UTF-32、UTF-16、またはデフォルトのutf-8)。RFC4627のセクション3では、どのエンコーディングを使われているのかを知る方法を示しています。

13.8. データをJSONファイルとして保存する

JSONは、JavaScriptにおける自分で定義したデータストラクチャに酷似しています。これは偶然ではありません。JavaScriptのeval()関数を使ってJSONシリアル化されたデータを"デコード"することもできます。(信頼されない入力に対する警告が起きることがありますが、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)                              # ③

# ① これまでのentryデータストラクチャを再利用せずに、新しくデータストラクチャを作ります。この後の章では、より複雑なデータストラクチャをJSONでエンコードしようとすると何が起きるかを見ていきます。

# ② JSONはテキストベースのフォーマットなので、開くときはテキストモードにして文字エンコーディングを指定する必要があります。utf-8で問題ありません。

③pickleモジュールと同様に、jsonモジュールにはdump()関数があり、Pythonデータストラクチャと書き込み可能なストリームオブジェクトを受け取ります。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ファイルはサイズが大きくなるものの、より読みやすくなります。インデントパラメータは整数で、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"
}

13.9. PythonデータタイプをJSONに対応させる

JSONはPython専用というわけではないので、Pythonデータ型が扱う部分とのミスマッチがいくつかあります。単なる名前の違いというものもありますが、2つの重要なPythonデータ型がjSONには存在しません。見つけられるでしょうか。

Notes JSON Python 3
object dictionary
array list
string string
integer integer
real number float
* true True
* false False
* null None
* All JSON values are case-sensitive.

何が無いかわかりましたか?タプルとバイトです!JSONにはarray型があり、jsonモジュールではPythonでのリストに対応させますが、「固定されたarray (タプル)」というデータ型が存在しません。また、JSONは文字列を充分にサポートしていますが、バイトオブジェクトやバイトアレイを全くサポートしていません。

13.10. JSONでサポートされていないデータ型をシリアル化する

ビルトインのJSONではバイトをサポートしていませんが、バイトオブジェクトはシリアル化できないという意味ではありません。jsonモジュールには、未知のデータ型をエンコード、デコードするための拡張ツールがあります。(ここで未知というのは、JSONで定義されていないという意味です。jsonモジュールがバイトアレイを知っているのは明らかですが、JSONの仕様で制限されているのです。)JSONがネイティブでサポートしていないバイト型やその他の型をエンコードしたい場合は、その型のためのカスタムのエンコーダ、デコーダを用意する必要があります。
①OK、それではentryデータストラクチャを再度見ていきます。
>>> 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 "", line 5, in 
  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

①OKですね。すべてが含まれています。ブール値、None値、文字列、文字列のタプル、バイトオブジェクト、タイムストラクチャです。
②先にも述べましたが、大事なことなので繰り返すと、JSONはテキストベースのフォーマットです。JSONファイルを開くときは常にテキストモードで、エンコードはutf-8にします。
③これは上手くいっていません。どうしたのでしょうか?

こんなことが起こっています。json.dump()関数は b'\xDE\xD5\xB4\xF8' をシリアル化しようとして、失敗しました。なぜならJSONはバイトオブジェクトをサポートしていないからです。しかし、保管したいバイトが重要であるならば、"ミニ・シリアル化フォーマット"を自分で定義すればよいのです。
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がネイティブにサポートしていないデータ型のために"ミニシリアル化フォーマット"を独自に定義するには、Pythonオブジェクトをパラメータとして受け取る関数を定義します。json.dump()関数がそのままではシリアル化できないオブジェクトそのものが、Pythonオブジェクトです。今の場合ではバイトオブジェクトb'\xDE\xD5\xB4\xF8'です。
②カスタムしたシリアル化関数では、json.dump()関数が渡したPythonオブジェクトの型を確認します。
厳密には、カスタム関数がシリアル化するデータ型が1つだけであれば、この確認は必要ありません。しかし、こうすることで関数がどの型を扱っているのかはっきりしますし、後になってさらにデータ型のシリアル化を追加するときもより簡単になります。
③この場合、バイトオブジェクトを辞書に変換します。
キー__class__は元のデータ型を(文字列'bytes'で)保持していて、キー__value__は実際の値を保持しています。もちろんこれがバイトオブジェクトであるはずはありません。重要なのはJSONでシリアル化することができる何かに変換することです。バイトオブジェクトは整数の連続で表され、各整数は0-255の範囲のいずれかです。list()関数を使ってバイトオブジェクトを整数のリストに変換することができます。 b'\xDE\xD5\xB4\xF8'は[222, 213, 180, 248]となります。(計算してみましょう!バイト16進数\xDEは10進数では222で、16進数\xD5は10進数で213、などとなります)。
④この行は重要です。シリアル化しようとしたデータストラクチャは、ビルトインのJSONシリアライザもカスタムシリアライザもどちらも扱えない型を持っているかもしれません。その場合、カスタムシリアライザは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 "", line 9, in 
    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()関数を定義したところにあります。
②テキストモード、uft-8エンコードです。(忘れる恐れがあります!私もときどき忘れてしまいます!失敗するまではすべて順調に進んでいきますが、失敗したら大惨事です)
③これは重要な部分です。カスタム変換関数をjson.dump()関数のデフォルトパラメータに入れています。(ほら、Pythonではすべてがオブジェクトだからです!)
④これは実際には動作しませんが、例外を確認しましょう。これ以降は、バイトオブジェクトがシリアル化できないとjson.dump()関数が不満を述べることはありません。
今では別のことに対して不満を述べています。time.struct_timeオブジェクトです。
他の例外が出ることは進歩ではないと思ったりしますが、そうではありません!ここを通過するにはもう1つ微調整がいります。
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なのかどうかを確認する必要があります。

②そうであれぱ、バイトオブジェクトのときにした似たような変換をすることになります。time.struct_timeオブジェクトを、jsonシリアル化された値だけの辞書に変換します。
この場合、datetimeを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}

13.11. JSONファイルからデータをロードする

pickleモジュールと同様に、jsonモジュールにもload()関数があり、ストリームオブジェクトを受け取ってJSONエンコードのデータを読み出し、JSONデータストラクチャをミラーリングした新しいPythonオブジェクトを作成します。
>>> shell
2
>>> del entry                                             # ①
>>> entry
Traceback (most recent call last):
  File "", line 1, in 
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}

# ① デモのためにPythonシェルを#2に切り替え、先ほどpickleモジュールで作ったdataストラクチャを削除します。
# ② 最もシンプルな例では、json.load()関数はpickle.load()関数と同じように動作します。ストリームオブジェクトを渡すと新しいPythonオブジェクトが返ります。
# ③ 良いニュースと悪いニュースがあります。まず良いニュースは、json.load()関数はPythonシェル#1で作ったentry.jsonファイルを読み込んで、そのデータを持った新しいPythonオブジェクトを作ることに成功しました。悪いニュースは、元々のentryデータストラクチャを再現していないということです。'internaI_id'、 'published_date'という2つの値は新しい辞書として作られています。具体的には、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

①この変換関数はパラメータを1つ受け取って値を1つ返しますが、受け取るパラメータは文字列ではなくPythonオブジェクトです。JSONエンコード文字列を逆シリアル化したPythonオブジェクトです。
②このオブジェクトにto_json()関数が作ったキー__class__が含まれているかを調べます。キーがあるならば、キー__class__に対する値を見れぱ、値をどのように元のPythonデータ型にデコードするかがわかります。
③time.asctime()関数が返した時刻文字列をデコードするには、time.strptime()関数を使います。この関数はフォーマットされたdatetime文字列を受け取り(カスタマイズされたフォーマットで、time.asctime()と同じデフォルトをもつもの)、time.struct_timeを返します。
④整数のリストをバイトオブジェクトに変換するためには、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'キーが入っていて、対応する値はバイトオブジェクトです。さらに'published_date'キーも入っていて、値はtime.struct_timeオブジェクトです。

ここで、最後の難関があります。
>>> 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()関数を逆シリアル化で使ったあとでも、オリジナルデータストラクチャの完全な複製は作れませんでした。なぜでしょうか?
# ② 元のentryデータストラクチャでは、'tags'キーの値は3つの文字列のタプルでした。
# ③ 一方、シリアル化を往復してきたentry2データストラクチャでは、'tags'キーの値は3つの文字列のリストです。JSONはタプルとリストの差を区別しません。リストのようなデータ型のアレイを持っているだけで、jsonモジュールはタプルもリストも何も言わずにjsonアレイに変換してシリアル化しているのです。普通に使っているときは、タプルとリストの差は無視することができますが、jsonモジュールを使うときには覚えておかなければなりません。

13.12. さらに読むには

👉pickleモジュールに関する多くの記事はcPickleに関しても書いてあります。Python2では、pickleモジュールには2種類の実装がありました。1つはピュアなPythonで書かれたもので、もう一つはCで書かれたものです(Pythonから呼び出すことができます)。Python3では、これら2つのモジュールは統合されたので、pickleをインポートするだけでかまいません。これらの記事が役に立つかもしれませんが、無用となったcPickleの情報は無視してください。

pickle モジュールについて:
On JSONjson モジュールについて:
pickleの拡張について:



0 件のコメント:

コメントを投稿