4. 2to3の先に

2to3は優れたツールですが、python3 セーフにするためには、 もう少し手を加える必要がある場合もあります。 特に制御関係ではバイナリデータを取り扱う場合に注意が必要です。

4.1. 文字型/バイナリ型データ

Python2ではC言語からの伝統を受け継いで(?)、文字型データ(str)とバイト型データ(bytes)には 本質的な違いはありませんでした。というか、bytesstrの別名となっています。 また、2byte文字を取り扱うために、unicode型が導入されています。 unicode型とstr型の変換はencode/decodeなどのメソッドを使って行います。

ところが、Python3では 文字列(str)は unicode型の同意語となります。unicode文字列を表すバイト型データ(bytes)はunicode文字列をencodeすることで得られます。 また、バイト列をunicode文字列(str)に変換するためには、decodeメソッドの助けを借りなければいけません。

ソースコード 4.1.1 Pythonでの文字型データ
>>> ("あ",u"あ", r"あ",b"\xe3\x81\x82")
("あ",u"あ", r"あ",b"\xe3\x81\x82")
('\xe3\x81\x82', u'\u3042', '\xe3\x81\x82', '\xe3\x81\x82')

>>> s="あいう"
s="あいう"
>>> type(s)
type(s)
<type 'str'>
>>> u=s.decode('utf-8')
u=s.decode('utf-8')
>>> type(u)
type(u)
<type 'unicode'>
>>> u
u
u'\u3042\u3044\u3046'
>>> s
s
'\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86'
>>>
ソースコード 4.1.2 Pythonでの文字型データ
>>> ("あ",u"あ", r"あ",b"\xe3\x81\x82")
("あ",u"あ", r"あ",b"\xe3\x81\x82")
('あ', 'あ', 'あ', b'\xe3\x81\x82')
>>>s="あいう"
s="あいう"
>>> type(s)
type(s)
<class 'str'>
>>> b=s.encode('utf-8')
b=s.encode('utf-8')
>>> type(b)
type(b)
<class 'bytes'>
>>> b
b
b'\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86'
>>>

Vers.

python2

python3

types

encode

decode

[0]

encode

decode

[0]

bytes

->str(b)

->u

str

N/A

->str(u)

int

str

->str(b)

-> u

str

->b

N/A

u

unicode

->str(b)

-> u

u

->b

N/A

u

通常の文字型データだけを取り扱っている限りでは、2to3の変換後のプログラムは問題なく動作するでしょう。しかしPython2でstr型データをbytes型データとして取り扱うために使っているプログラムでは、2to3は自動変換を行うことができません。str型データとして取り扱うべきか(その場合にはUnicode型=python3のstr型に変換),bytes型データとして取り扱うべきか(その場合にはPython3では明示的にbytes型を指定する必要がある)をpython2のソースから自動的に判断することが 2to3 にはできないためです。

py2/p3で両立するためには、bytesとunicodeを明示的に使い、 bytes -> unicodeの時にdecodeを、unicode-> bytesの変換でencodeを使うと決めておけば、 2to3はそれを頼りに変換を行います。 2to3適用前にこれを実施しておけば、あとは何度2to3を通しても問題無いはずです。

  • 文字列として使う場合は、unicode

  • バイト列として使う場合は、bytes

cVXI11EPICS CAモジュールではpython2/python3の双方をサポートするために、この様な変更を加えています。 また、cVXI11EPICS CAモジュールはsetup ツールを使ったbuild時に2to3ツールを自動的に適用する様にしています。 urlliburlopen()などから返される値も:py:class:`bytes型のデータとなっていますので、 テキストとして処理する前に:py:meth:`decode メソッドを使って strに変換して置きましょう。

リテラルなバイト列表現 (b"abc"など)はPython2, Python3の双方で利用可能です。 バイト列として取り扱う定数データは、この表現にして置くことで、python2/python3の双方でほぼ同じ様に取り扱うことができます。 バイト列(byte)から1バイトのデータを取り出す際には、注意が必要です。Python2では[n]オペレータで1バイトのデータをバイト列(=str)を 取り出すことができます。しかし、python3ではbyte列から[n]オペレータを使って1バイトデータを取り出すと、int型のデータとなってしまいます。 一方[n:n+1]で一バイト分のデータを取り出した場合には、bytes型になっています。  この様な動作の違いを吸収するためには、 python2でも1バイトのデータを取り出す場合にも[n:n+1]を使っておくという配慮が必要です。

また、python2/python3とも bytearray 型を持っています。bytearray型はPY2/PY3で概ね同じmethodsを持っている(例外:copy,clear,maketrans)ので、 byte型ではなく、bytearray型を積極的に使うのもpython2/pytho3互換とするためには有効と考えられます。

4.1.1. openのencoding

文字型/bytes列の取扱で注意すべき事として、open関数のpython2/python3での 違いがあります。

python3において、文字型データのファイル入出力では、入出力モードがテキストモード("r"あるいは"w")である場合には、ファイルが期待するエンコーディングへの変換が必要となります。 デフォルトのエンコーディングはプラットフォーム依存で, locale.getpreferredencoding(False) の返す値が使われます。 バイナリデータのファイル入出力ではバイナリモード("rb",'wb"など)を 使うことが推奨されています。また、そうしておけば、python2/python3の双方で想定した通りの 動作をするでしょう。

ソースコード 4.1.1.1 python2 でのopen()関数のプロトタイプ
>>> help(open)
open(...)
          open(name[, mode[, buffering]]) -> file object

#デフォルトのエンコーディング
>>> locale.getpreferredencoding(False)
locale.getpreferredencoding(False)
'US-ASCII'
ソースコード 4.1.1.2 python3 でのopen()関数のプロトタイプ
>>> help(open)
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
    Open file and return a stream.  Raise OSError upon failure.

#デフォルトのエンコーディング
>>> locale.getpreferredencoding(False)
locale.getpreferredencoding(False)
'UTF-8'

4.1.1.1. Python2/Python3の判定

python2/python3の双方に対応するプログラムを開発するためには、 今動作中の環境を判定する必要が出てきます。その場合には次のコードを 使うのが良さそうです。

import sys

PY3 = sys.version_info > (3,)

これで, 変数PY3はpython2では False,python3では、Trueとなっています。 sys.version_infoは2.7以降はversion_info型のオブジェクトですが、2.6あるいはそれ以前のバージョンでは、tupleとなっています。 しかし、上記の式はいずれの環境においても正しく動作します。