Python 3.0 Hacks

第4回「俺様プチencoding」実装して理解するPython3.0のioとcodec、encodingの機構

Python3.0と2.xでstrの意味が変わりました。それに伴い文字列処理がどうかわるかを「俺様プチencoding」を実装することで、垣間見ることにしましょう。

Python3.0でのstr/unicode/bytes

第0回でUnicodeの識別子について触れられていましたが、attributeの名前に「鳴く」はself.__dict__等で取り出すことができます。Pythonで扱われる文字列そのものがUnicodeなのです。⁠生」のバイトがほしいときはbytesを使います。

この関係を端的に言い表しているのが表1で、Python3kのインタプリタが2.xのコードベースで作られていることとあわせて考えると、状況がよく理解いただけると思います。同じ実装(C nameはPythonのreference実装であるCPythonのsource code上での型)が違う呼び名str/bytesもしくはunicode/strになっているのが読み取れます。

表1 文字列・バイト関係の名称変遷PEP3137より抜粋)
C name 2.x 3.0a1 3.0a2
namerepr namerepr namerepr
PyUnicodeunicodeu'' str''str''
PyStringstr'' str8s''bytesb''
PyBytes該当なし bytesb''bytesarraybytesarray(b'')

2点ほど補足。

  • Pythonのstr/bytes/unicodeはimmutableで、実装効率の上でmutableなbytesが必要になった場合はbytesarrayを使用します。
  • reprはなんらかobjectを、そのobjectを生成させるcode片を印刷させたいときに使う関数です。この表ではそれぞれのversionのpythonで該当のobjectに対してreprを呼んだときの出力のformatです。2.xでは''がPyStringであったが、3.0では''はPyUnicodeを印刷するために使われています。

codecを拡張して「俺様プチencoding」

記事の終わりに今回実装するencodingのソースコードへのリンクを用意してあります。ここではお昼ごはんを食べながらでも読めるように「sourceにinline」な形式で実装を見ていきます。

import codecs

普段、Pythonでは何気なくimportしてpackage/moduleを使っていますが、codecは複雑な実装になっています。どうなっているかというと、ソースパッケージにおいてLibの下にある、Pythonで実装されたcodecはencodingを管理するためのモジュールで、encodingの名前と実装のマッピングはencodingsパッケージで行われています。そして実装の本体はCで書かれており、_codecmodule.cとして存在し、encodings moduleにおいてcodecという名前でimportされています。

EGG = b'0x00'
SPAM = b'0xff'

今回作る「俺様プチencoding」は、0x00と0xffのみからなります。0x00をEGG、0xffをSPAMと名付けます(3、4行目⁠⁠。b''はbytes定数を書くために3.0から導入された記法です。bytes()を呼んでもよいですが、実行時までチェックされなくなるのでエラーの元でしょう。動的に生成しないならできるだけb''を使いましょう。空行は紙面稼ぎではなく、適度にPEP8(PythonのCoding Style Guide)にしたがって書いたコードを分割しているためにできています。

def monty_encode(input, error='strict'):
    consumed = 0
    output = bytearray()
    while input:
        if input.startswith('egg'):
            output.extend(EGG)
            read = 3
        elif input.startswith('卵'):
            output.extend(EGG)
            read = 1
        elif input.startswith('spam'):
            output.extend(SPAM)
            read = 4
        else:
            assert False
        consumed += read
        input = input[read:]
    return output, consumed

いきなりですが、encoderの実装です。decoderはナシです。Unicodeからこのencodingへの変換を実装しています。inputはUnicode文字列で、返すものは、生成したバイト列と与えられた文字列の中から読んだ消費した文字列の長さのペアです。上のレイヤのバッファのindexが進むのでしょう。実用的なencodingを処理する場合ならば、状態を持ったりそれなりの規模のテーブルをルックアップしたり、もしくは単に線形にマップする必要があるでしょう。この例では2つしかバイト列がなく、対応するUnicodeの列も3種類しかないのでこんなelifでかけてしまいます。

class Codec(codecs.Codec):
    encode = monty_encode
    decode = None

インターフェースはcodec.Codecで定められており、これを継承して実装します(25行目⁠⁠。エンコードしか実装していないのでdecodeにはNoneを入れます。3.0からはNoneがkeywordになったり、数との比較ができなくなったり(2項のtypeが比較できないと駄目、この辺は第2回で出てきたABCが関係してきます)しています。関数は暗黙のうちにNoneを返しますが、2.xでこれを悪用していると比較が利かないので、3.0では動きません。

class IncrementalEncoder(codecs.IncrementalEncoder):
    def encode(self, input, final=False):
        return monty_encode(input, self.errors)[0]

class IncrementalDecoder(codecs.IncrementalDecoder):
    pass

class StreamWriter(Codec,codecs.StreamWriter):
    pass

class StreamReader(Codec,codecs.StreamReader):
    pass

class StreamConverter(StreamWriter,StreamReader):
    encode = None
    decode = monty_encode

Streamを使ったEncoder/Decoderは効率を上げるための実装なので、今回はほぼスキップです。

def getregentry():
    return codecs.CodecInfo(
        name='monty',
        encode=Codec.encode,
        decode=Codec.decode,
        incrementalencoder=IncrementalEncoder,
        incrementaldecoder=IncrementalDecoder,
        streamwriter=StreamWriter,
        streamreader=StreamReader,
    )

encodingを実装するCodecインスタンスはCodecInfoを使って管理されています(関数getregentry⁠⁠。いままでの実装を束ねてあげます。

def mysearch(encoding):
    if encoding == 'monty':
        return getregentry()
    else:
        return codecs.search_function(encoding)

codecs.register(mysearch)

標準でencodingの名称からcodecを解決するためにregisterされている関数(codecs.search_function)をwrapして、今回作ったcodecが返されるようにしてあげましょう。そしてcodecs.registerでその関数を登録します。ここまでで実装は終わりです。

ioモジュール概要

さて、作ったらテストしないといけませんが、そのためには、encodingを持った何らかの文字列バッファを作ってそこに書き込んでやる必要があります。この手の用途には、2.xではStringIO.StringIOを使ってきましたが、3.0ではio moduleを使います。

図1 ioモジュールのクラス図
図1 ioモジュールのクラス図

詳細は省きますが、おえておくべき点は次の通り。

  • IOBase継承の頂点にある。
  • Bufferされているioの親はBufferedIOBaseである。
  • encodingがあるIO classの親はTextIOBaseである。
  • on memoryなBufferの実装BytesIOはbytesarrayを使っている。
  • 効率が要求されるものはCで実装が提供されている。

ところで、標準入出力sys.stdinとsys.stdoutはio.TextIOWrapperのインスタンスです。そしてそのデフォルトのencodingは端末のencodingです。バイナリを返すcgiでsys.stdoutに書き出す際にはsys.stdout.bufferにbytesで書きます。Encodingを指定する場合はEncodedFileが役に立つでしょう。この辺は対応していく過程でどう扱われるのか興味深いです。

http://docs.python.org/3.0/library/sys.html#sys.stdoutより

Note The standard streams are in text mode by default. To write or read binary data to these, use the underlying binary buffer. For example, to write bytes to stdout, use sys.stdout.buffer.write(b'abc')

バッファを用意し、codecをテストする。

テストに使う文字列バッファはencodingとBytesIOの組で実現します。io.StringIOのencodingはutf-8固定ですので、今回は作ったencodingに変換させるために、io.StringIOの一歩手前のTextIOWrapperを使います。このTextIOWrapperはencodingをbytesに付け加えるデコレーションを担っています。ですので、TextIOWrapperに与えるBufferWriterやBytesIOはencodingを持っていません。BufferWriterやBytesIOのwrite methodに与えるobjectの型はbytesもしくはbytearrayになります。

if __name__ == '__main__':

Pythonでは非常によく出てくるトリック。ifでガードされたthen節はコマンドラインでpython [該当のsource file]としたときだけ実行される。テストコードは外部のプログラムからimportする際(?)には実行してほしくないので、ガードします。

    import io
    raw = io.BytesIO()
    buffered = io.BufferedWriter(raw)
    x = io.TextIOWrapper(buffered, encoding='monty', errors="strict", newline=None)

メモリ上のバッファとしてのBytesIOのインスタンスを生成し(raw、67行目⁠⁠、Bufferされた書き込みができるようにBufferWriter(buffered、68行目)でwrapしてあげます。TextIOWrapperのインスタンスをいま作ったencodingの名前、'monty'を指定して生成します(69行目⁠⁠。

    x.write('egg')
    x.flush()
    assert x.buffer.raw.getvalue() == EGG
    x.write('spamspam')
    x.flush()
    assert x.buffer.raw.getvalue() == EGG + SPAM + SPAM
    x.write('卵')
    x.flush()
    assert x.buffer.raw.getvalue() == EGG + SPAM + SPAM + EGG

あとはこのバッファに"spam"、"egg"もしくは"卵"をunicodeで渡してあげましょう。俺様codecが仕事をしてBytesIOのインスタンスにEGGとSPAMを意味するバイトの0x00と0xffが書き込まれます。今回のプログラムのソースは以下からどうぞ。

おすすめ記事

記事・ニュース一覧