2011-12-12

six

2011 Python アドベントカレンダー のエントリです。Python 2 と Python 3 の差異を吸収するラッパ集として six というライブラリを触ってみました。

Python 3 に同梱されている 2to3 というツールは、ひとつの Python 2 のソースから、 Python 3 用のソースを自動生成します。したがって、もとの Python 2 用コードと Python 3 用コードのふたつのバージョンを持つことになります。

一方、six がやろうとしているのは、ひとつのソースコードを Python 2 と Python 3 からも呼び出せるようにすることです。提供されるのは、基本的な定数や、ラッパ関数です。

文字列リテラルのフェイク

Python 3 と言えば文字列です。

# coding: utf-8

print(u'うほ')

Python 3 ではこれはエラーになります。six.u() 関数は、文字列リテラルのフェイクになってくれるそうです。

# coding: utf-8

import six
print(six.u('うほ'))

実行してみましょう。

$ python2.7 foo.py

ãã»
$ python3.2 foo.py
うほ

えー!six.u() の引数は、 unicode-escape でエンコードされた文字列じゃないとだめだそうです。「うほ」の場合は "\\u3046\\u307b" です。これはあんまりじゃないかと。しかも、

$ python2.7 foo.py
うほ
$ python3.2 foo.py
\u3046\u307b

ええぇぇ。そりゃまあ、リテラル表記を関数で代用するんですから、大変だと思いますよ。

見なかったことにして、six.b() に行きましょう。これは b'…' のフェイクです。

x = six.b('AB')
print(x[0]+x[1])
$ python2.7 foo.py
AB
$ python3.2 foo.py
131

oh… このあたりの挙動は 石本さんのブログ で書かれていますが、リテラルのフェイクは難しいようです。

気をとりなおして別の関数を見ましょう。それは文字列なのか、それともバイト列なのかを知りたいとき、isinstance とかを使うわけです。が、Python 3 では、このあたりの型や継承関係が変わっています。

x = u'foo'
if isinstance(x, basestring):
    print(x.encode('ascii'))

y = b'ABC'
if isinstance(y, str):
    print(y.decode('ascii'))

Python 2 では、上のように書いたりしたんですが、Python 3 では basestring は存在しませんし、str はバイナリを表しません。そこで、six の text_type と binary_type を使います。

x = six.u('foo')
if isinstance(x, six.text_type):
    print(x.encode('ascii'))

y = six.b('ABC')
if isinstance(y, six.binary_type):
    print(y.decode('ascii'))

実行してみると…

$ python2.7 foo.py
foo ABC $ python2.7 foo.py b'foo' ABC

お、これはわりとちゃんと動きますね。text_type は unicode (py2) か、str (py3) のエイリアスになっています。binary_type は同じく str と bytes です。

辞書のイテレータ

Python 2 の iteritems, iterkeys, itervalues は、Python 3 では items, keys, values に名前が変わっています。そして、Python 2 の items, keys, values はなくなっています。

この差異を吸収するのが、 six.iteritems などの関数です。

for k,v in six.iteritems({'spam':99, 'ham':0}):
    print(k,v)
$ python2.7 foo.py ('ham', 0) ('spam', 99) $ python3.2 foo.py ham 0 spam 99

辞書のメソッドを使わずに、six.iter* を使うことで、必ずイテレータを返すようになります。

表示が違うのは、print(k,v) の解釈の違いです。 Python 2 では、タプル (k,v) をプリントしています。一方、Python 3 では、k と v ふたつの引数をとって、それぞれをスペース区切りで表示します。

six には、これを吸収する関数もあります。

six.print_(1,2,3)

実行すると…

$ python2.7 foo.py
1 2 3
$ python3.2 foo.py
1 2 3

まとめ

言語仕様が変わって、オフィシャルに 2to3 が用意されるくらいですから、ラッパくらいでは吸収し切れない、というのがよく分かりました。個人的な感想は、2to3のほうが筋がいいんではないか、と思いました。

あしたは @nonNoise さんにバトンタッチです。よろしくお願いします。

2011-12-13 追記: pre タグがおかしかったのなど、諸々修正。blogspot って wysiwig エディタでコード書くのがいまいち。