銀月の符号

Python 使い見習いの日記・雑記

標準コーデックとバイト列

半年ほど前に『「XOR をとるコーデック」のファクトリ - 銀月の符号』なんて作ったなーというのを思い出して。

ふたたび遊んでみようとドキュメントを読んでいたところ Python 3 では標準コーデックの種類が減っているのに気づいた。

zlib_codec, string_escape, rot13 などバイト列からバイト列に変換するコーデックが軒並み削除されている模様。 Python 3 のコーデックが行うのは文字列(ユニコード文字列)とバイト列間の変換のみらしい。

Python 3.1.1 (r311:74483, Aug 17 2009, 17:02:12) [MSC v.1500 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> str.encode
<method 'encode' of 'str' objects>
>>> bytes.decode
<method 'decode' of 'bytes' objects>
>>>
>>> bytes.encode
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'bytes' has no attribute 'encode'
>>> str.decode
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'str' has no attribute 'decode'

なるほど。文字列(ユニコード文字列)の decode メソッド、バイト列の encode メソッドが削除されている。前作った xor するコーデックも Python 3 系が普及した際には動かせなくなるようだ。

子ネタ、一部のコーデックは codecs.open ですこしずつ入出力を行うとバグる

codecs.open が生成する codecs.StreamReaderWriter が拠り所としている codecs.Codec は状態保持をしないと定められている。このため、一部のコーデックはすこしずつ入出力をするような使い方をすると誤動作する。たぶん Python 3 で消えた、データを圧縮する類のコーデック限定。たとえば zlib_codec 。

Python 2.6.2 (r262:71605, Apr 14 2009, 22:40:02) [MSC v.1500 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import codecs
>>> with open('spam', 'rb') as r:
...   with codecs.open('ham', 'wb', 'zlib_codec') as w:
...     while True:
...       temp = r.read(128)
...       if not temp:
...         break
...       w.write(temp)
...
>>> open('spam', 'rb').read() == open('ham', 'rb').read().decode('zlib_codec')
False

ちなみに zlib_codec や bz2_codec は codecs.open や codecs.getreader, codecs.getwrite を使わずに codecs.getincrementalencoder, codecs.getincrementaldecoder を使えば誤動作しない。ただし、入出力が終わったかどうかを自前で判定できない、というか判定しない仕様になっている。なので final=True を引数に指定することで入出力が終わったことを明示しなければならない。使い方は以下。

>>> import codecs
>>> encoder = codecs.getincrementalencoder('zlib_codec')()
>>> with open('spam', 'rb') as r:
...   with open('ham', 'wb') as w:
...     while True:
...       temp = r.read(128)
...       if not temp:
...         break
...       temp2 = encoder.encode(temp)
...       w.write(temp2)
...     temp2 = encoder.encode('', final=True)
...     w.write(temp2)
...
>>> decoder = codecs.getincrementaldecoder('zlib_codec')()
>>> with open('ham', 'rb') as r:
...   with open('eggs', 'wb') as w:
...     while True:
...       temp = r.read(128)
...       if not temp:
...         break
...       temp2 = decoder.decode(temp)
...       w.write(temp2)
...     temp2 = decoder.decode('', final=True)
...     w.write(temp2)
...
>>> open('spam', 'rb').read() == open('eggs', 'rb').read()
True

zlib_codec らがこれで動くのは codecs.IncrementalEncoder, codecs.IncrementalDecoder の encode, decode メソッドがただしくオーバーライドされているから。ここで手抜きをされている uu_codec や base64_codec などはこれでも動作しないので、ファイルの内容をすべて読み込んで一気に変換しなければならない。

落とし穴満載。今は codecs モジュールのソース読んだ直後だから大丈夫だけれど、数ヵ月後に触ったらまたはまりそう。

ということで結論。バイト列関連のコーデックは codecs モジュールを通してつかうのは控える。 zlib, bz2, binascii といったモジュールを直接使えば問題ない。それに、 Python 3 系にはないのだから。