読者です 読者をやめる 読者になる 読者になる

CUBE SUGAR CONTAINER

技術系のこと書きます。

Python のバージョン毎の違いとその吸収方法について

Python

この記事の目指すところ

現在 Python はバージョン 2.x 系と 3.x 系という、一部に互換性のないふたつのメジャーバージョンが併用されている。 その上で、この記事にはふたつの目的がある。

ひとつ目は、2.x 系と 3.x 系の違いについてまとめること。 現状、それぞれのバージョン毎の違いはまとまっているところが少ない。 自分用に、このページだけ見ればひと通り分かる!っていうものがほしかった。

ふたつ目は、2.x 系と 3.x 系の違いを吸収するソースコードの書き方についてまとめること。 こちらも Web 上にナレッジがあまりまとまっていない。 これについては今 python-future というパッケージがアツい。

尚、サポートするバージョンは以下の通り。

  • 2.x 系: 2.6 と 2.7
  • 3.x 系: 3.3 と 3.4

本題に入る前に、最近の Python 事情についてまとめてみる。

最近の Python 3 について

最近は主要なライブラリのほとんどが 3.x 系の対応を完了しているため、メインで 3.x 系を使っても支障のない状況ができあがりつつある。 以下を見ると PyPI でダウンロード数上位にあるライブラリの Python 3 対応状況がわかる。

Python 3 Wall of Superpowers

最近の Python 2 について

一方で、2.x 系の最終マイナーリリースとなるバージョン 2.7 のサポートは 2020 年まで延長された。 また、バージョン 2.6 は 2013 年 10 月に EOL を迎えているにも関わらず、それを搭載した OS (例えば RHEL6/CentOS6) はまだまだ現役にある。 もちろんバージョン 2.7 が 2020 年に EOL を迎えたとしても、それを搭載した RHEL7/CentOS7 のサポートは 2024 年まで続く。 これから言えることは、すなわち少なくともこの先 10 年間 (2025 年頃まで) は 2.x 系と付き合い続けることになるということ。

この先もずっと Python 2 と付き合いを続ける必要のある状況は限られる (はず)

とはいえ、全ての開発者が 2.x 系と今後も付き合いを続けていく必要があるかというとそうではない。 Fedora や Ubuntu といった Linux ディストリビューションは将来のバージョンでデフォルトの Python バージョンを 3.x 系にする計画を立てている。 今後も 2.x 系を使い続けなければならない状況は、おそらくふたつのパターンに絞られてくるはず。

  • 開発ターゲットの OS が古いなどで 2.x 系で動作する必要がある
  • 開発対象がライブラリのため幅広い環境で動作する必要がある

無理に 2.x 系をサポートする必要がないのであれば 3.x 系だけをサポートすればいい。 サポート対象が 2.x 系か 3.x 系のどちらかだけに絞られる場合は互換性について考える必要がないので、後はひたすら開発するだけになる。 この状況であればさほど難しくはない。

互換性に悩む必要のある状況について

今後か、あるいは一時的にせよ 2.x 系と 3.x 系の互換性について悩む必要がある状況は以下のパターンだと思う。

  • これから 2.x 系と 3.x 系に対応したソースコードを書き始める
  • 2.x 系に対応したプロジェクトを 3.x 系に移行する
  • 2.x 系に対応したプロジェクトを 3.x 系でも動くようにする
  • 3.x 系に対応したプロジェクトを 2.x 系に移行する (あるのか?)
  • 3.x 系に対応したプロジェクトを 2.x 系でも動くようにする

対処方法について

2.x 系と 3.x 系の互換性のなさにどう対処するかは以下のパターンに分けることができて、それぞれに長所と短所がある。

  • 愚直になんとかする
    • 内容: エラーになる箇所を ImportError や if 文を駆使して直す
    • 長所: 短いコードの場合は対処が早くおわる
    • 短所: 長いコードの場合に複雑になって可読性が落ちる
  • 外部の互換レイヤーパッケージ/ツールを使用する
    • 内容: 2.x 系と 3.x 系の違いを吸収する外部のパッケージを利用する
    • 長所: コードの可読性の低下を防ぐことができる
    • 短所: プロジェクトの依存パッケージが増える
  • プロジェクト内に独自の互換レイヤーを用意する
    • 内容: プロジェクト内に 2.x 系と 3.x 系の違いを吸収するモジュールを作る
    • 長所: 依存パッケージを増やすことなく可読性の低下を防ぐことができる
    • 短所: 別のパッケージで既に存在する機能を再発明することになる

ちなみに、愚直になんとかするパターンは相当険しい道のりになるのでおすすめできない。

作業をする上で必要になるもの

上記のどういったパターンを取るかに関わらず、以下は必須となる。

  • 2.x 系と 3.x 系の違いに関する知識
    • 言わずもがな
  • 自動化されたテスト
    • 対応状況を確認する方法は実際にソースコードを動かしてみる他にない

また、以下のツールは使うことが好ましい。

  • pylint
    • 自動変換した後でソースコードの質が悪くなっていないかを確認した方がいい
  • tox
    • 複数のバージョンの Python でテストを走らせることができる
  • caniusepython3
    • プロジェクトの依存パッケージに Python 3 対応していないものがないか確認できる

2.x 系と 3.x 系を共存させる場合の基本戦略

単一のソースコードで 2.x 系と 3.x 系の両方に対応させる場合の基本戦略について。 これからは 3.x 系の書き方を基本とした上で、それを同時に 2.x 系でも動くようにする作戦がおすすめ。

  • これから主流になる 3.x 系を使って開発できる
  • 標準ライブラリの中にそれをアシストするものが用意されている

以前は 2.x 系の書き方をしたソースコードを動的に 3.x 系に対応する形に変換するといったやり方もあった。 ただ、このやり方にはいくつか問題があるので今はおすすめできない。

  • 古いバージョンとなった 2.x 系を使って開発することになる
  • 自動変換は完璧じゃない

ちなみに、これは後述する 2to3 を setuptools の use_2to3 パラメータと組み合わせる方法で実現できる。

使用するツールについて

ここでは 2.x 系と 3.x 系の違いを吸収したり、あるいは各バージョンのソースコードを自動で変換してくれるツールを紹介する。 尚、自動変換に関しては完璧ではないので作業の大部分を自動化する程度の認識にとどめておいた方が良い。

2to3

2to3 は 2.x 系で動作するソースコードを 3.x 系で動作する形に自動で変換するツール。 Python に同梱されているのでインストールの必要がない。 変換は一方的なもので、ツールにかけたソースコードは 3.x 系でしか動かなくなる。

現在 Python でデファクトスタンダードになっているパッケージングライブラリの setuptools は 2to3 とのインテグレーションを提供している。 これはセットアップスクリプト (setup.py) で use_2to3 パラメータに True を指定しておくと、そのパッケージを Python 3 環境にインストールしようとしたときに自動でソースコードを 2to3 にかけてくれるというもの。 便利なんだけど前述した通り今から使うのはおすすめできない。

3to2

3to2 は 2to3 と逆で 3.x 系で動作するソースコードを 2.x 系で動作する形に自動で変換してくれる。 Python に同梱されているわけではなくサードパーティー製のパッケージなので、パッケージマネージャ (easy_install や pip) を使ってインストールする必要あり。

setuptools は 3to2 のインテグレーションを現時点で提供していないので手動で動かすパターンのみ。

python-modernize

python-modernize は 2to3 をベースに、2.x 系との互換性も考えて変換してくれるサードパーティ製のツール。 2.x 系との互換性を持たせるのに __future__ パッケージや、後述する six を駆使する。

six

six は 2.x 系と 3.x 系の違いを吸収するためのサードパーティ製のパッケージ。 例えば 2.x 系と 3.x 系で名前が異なる標準ライブラリは six 経由でインポートすると同じ名前が使えるようになる。

幅広いバージョンで使えることを志向しているため、サポートするバージョンが 2.4 以上と古いのも特徴になっている。

python-future

python-future は six と同様に 2.x 系と 3.x 系の違いを吸収するためのサードパーティ製のパッケージ。 違いとしては、モンキーパッチなどを駆使して six よりも更にアグレッシブに違いを吸収する。 例えば 2.x 系のソースコードでプリミティブ型の挙動を 3.x 系のものに書き換えたり、リネームされた後の名前で標準ライブラリをインポートできるようになる。 また、2to3 や python-modernize のように 2.x のコードを 3.x でも動くようにする自動変換する futurize コマンドや、反対に 3.x のコードを 2.x でも動くようにする pasteurize コマンドが搭載されている。

サポートするバージョンは 2.x 系が 2.6 以上で 3.x 系が 3.3 以上と現実的な割り切りを感じる。

下準備

今回は 2.x 系と 3.x 系の違いの他に python-future を使った両方のバージョンで動作する書き方を紹介する。 そのため、まずは python-future を pip でインストールしておこう。

$ pip install future

バージョン 2.x 系と 3.x 系の違いと両方で動作する書き方について

ここからは 2.x 系と 3.x 系の違いと、その両方で動作する書き方について紹介していく。

print

2.x

2.x では print は関数ではなく文になっている。

>>> print 'Hello, World!'
Hello, World!

2.x/3.x?

両方のバージョンで動かすには括弧で囲めばいいかというと、これだけでは不十分な場合がある。

>>> print('Hello, World!')
Hello, World!

2.x/3.x

3.x の関数で使えるパラメータを 2.x でも使えるようにするには from __future__ import print_function をインポートする必要がある。 ちなみに __future__ パッケージは 3.x の機能をバックポートしたもの。

>>> import sys
>>> from __future__ import print_function
>>> print('Hello, World!', file=sys.stderr)
Hello, World!

'/' 演算子

2.x

2.x では '/' 演算子が返す型が整数値になっている。

>>> 1 / 1
1
>>> 1 // 1
1

3.x

それに対して 3.x では浮動小数点型になっている。

>>> 1 / 1
1.0
>>> 1 // 1
1

2.x/3.x

この違いを吸収するには '//' 演算子を使うか、あるいはバックポートを利用して挙動を 3.x に寄せる。

>>> from __future__ import division
>>> 1 / 1
1.0
>>> 1 // 1
1

整数型

2.x

2.x には整数型に int と long のふたつの種類があった。

>>> 1
1
>>> 2 ** 64
18446744073709551616L
>>> int(2 ** 64)
18446744073709551616L

3.x

3.x では整数型が int に統一されている。

>>> 1
1
>>> 2 ** 64
18446744073709551616
>>> int(2 ** 64)
18446744073709551616

2.x/3.x

手っ取り早くどちらでも動くようにしたい場合には python-future の builtins パッケージから int をインポートすると良い。 これで 2.x で動かした場合にも大きな値が int (に相当する上書きされた型) として振る舞う。

>>> from builtins import int
>>> int(2 ** 64)
18446744073709551616

型のチェックをする際にも long というシンボルを使うと 3.x ではエラーになってしまう。 そこで future.utils から integer_types を読み込んで使う。

>>> from future.utils import integer_types
>>> isinstance(2 ** 64, integer_types)
True

例外処理

例外処理まわりはバージョン毎に書き方や API が結構変わる。

raise

2.x

2.x 系では例外を raise するときに例外クラスとメッセージを ',' で区切る書き方ができた。

>>> raise Exception, 'Oops!'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: Oops!

2.x/3.x

3.x 系では上記の書き方ができないので、このようにする。

>>> raise Exception('Oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: Oops!

catch

2.x

2.x 系ではクラスの後に ',' を入れて変数名をつけると補足した例外オブジェクトのインスタンスを取得できた。

>>> try:
...     raise Exception('Oops!')
... except Exception, e:
...     raise
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
Exception: Oops!
2.x/3.x

3.x 系では ',' が別の意味になって動作しないため、代わりに as キーワードを使う。 ちなみに別の意味というのは、',' で例外クラスを区切って羅列することで複数の例外をひとつのブロックで処理できるようになった。

>>> try:
...     raise Exception('Oops!')
... except Exception as e:
...     raise
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
Exception: Oops!

8 進数

2.x

整数値のはじめに '0' をつけて 8 進数として認識されるのは 2.x だけ。

>>> 0644
420

2.x/3.x

3.x では上記が動かないので '0' の代わりに '0o' をつける。

>>> 0o644
420

バックティック repr

2.x

オブジェクトを '`' で囲むと repr() 相当の動作になるのは 2.x だけ。

>>> `object()`
'<object object at 0x109f2e0e0>'

2.x/3.x

どちらのバージョンでも動くようにするには組み込み関数 repr() を使う。

>>> repr(object())
'<object object at 0x10a7eb070>'

相対インポート・絶対インポート

3.x ではインポートの挙動が一部変わった。 これはちょっと説明が大変なんだけど、パッケージの同じディレクトリにあるモジュールを import {name} という風にインポートしていると 3.x で動かなくなる。

実際に試してみる

説明が難しいので実際に試してみる。 pkg という名前の Python パッケージを作る。

$ mkdir pkg
$ touch pkg/__init__.py

pkg の中に foo というモジュールを作る。 この中では bar というモジュールをインポートして使っている。 このインポートの仕方が問題。

$ cat << EOF > pkg/foo.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import bar


def main():
    bar.greet()


if __name__ == '__main__':
    main()

EOF

foo と同じ場所に bar というモジュールを作る。

$ cat << EOF > pkg/bar.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def greet():
    print('Hello, World!')

EOF

上記のパッケージをインストールするためのセットアップスクリプトを書く。

$ cat << EOF > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup
from setuptools import find_packages


def main():
    setup(
        name='pkg',
        version='0.0.1',
        description='Sample package',
        author='momijiame',
        author_email='momijiame@example.jp',
        packages=find_packages(),
    )


if __name__ == '__main__':
    main()

EOF

ディレクトリ構成は以下のようになっている。

$ find .
.
./pkg
./pkg/__init__.py
./pkg/bar.py
./pkg/foo.py
./setup.py

2.x の環境にインストールすると pkg.foo をインポートできる。

$ python --version
Python 2.6.9
$ python setup.py install 
$ python -c "from pkg import foo;foo.main()"
Hello, World!

が、これが 3.x の環境ではエラーになる。

$ python --version
Python 3.3.6
$ python setup.py install
$ python -c "from pkg import foo"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "./pkg/foo.py", line 6, in <module>
    import bar
ImportError: No module named 'bar'

2.x では import がトップレベルのパッケージ以外に同じディレクトリも探索の対象になるが、3.x ではトップレベルのパッケージしか探索の対象にならない。

3.x 対応で焦らないためには future.absolute_import をインポートしておく。

$ cat << EOF > pkg/foo.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import

import bar


def main():
    bar.greet()


if __name__ == '__main__':
    main()

EOF

これで 2.x の環境でもインポートの探索が 3.x と同様に行われるようになる。

$ python --version
Python 2.6.9
$ pip uninstall pkg -y && python setup.py install
$ python -c "from pkg import foo;foo.main()"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "pkg/foo.py", line 6, in <module>
    import bar
ImportError: No module named bar

bytes

bytes の仕様変更は一番厄介かもしれない。

2.x

2.x では bytes は str の単なるエイリアスに過ぎない。

>>> bytes(b'a') == str(b'a')
True
>>> type(bytes(b'a'))
<type 'str'>
>>> issubclass(bytes, str)
True
>>> list(bytes(b'ABCD')) == [65, 66, 67, 68]
False

3.x

それに対し 3.x では専用のオブジェクトになっている。

>>> bytes(b'a') == str(u'a')
False
>>> type(bytes(b'a'))
<class 'bytes'>
>>> issubclass(bytes, str)
False
>>> list(bytes(b'ABCD')) == [65, 66, 67, 68]
True

2.x/3.x

この問題を防ぐには python-future の提供する builtins.bytes をインポートしておくとよさげ。 これで 2.x であっても 3.x と同様の挙動になる。

>>> from builtins import bytes, str
>>> bytes(b'a') == str(u'a')
False
>>> type(bytes(b'a'))
<class 'future.types.newbytes.newbytes'>
>>> issubclass(bytes, str)
False
>>> list(bytes(b'ABCD')) == [65, 66, 67, 68]
True

インポートすると bytes が上書きされるので、元々の型がほしいときは future.utils.native() 関数を使うか future.utils.native_bytes 型を使う。

>>> from builtins import bytes
>>> type(bytes())
<class 'future.types.newbytes.newbytes'>
>>> from future.utils import native
>>> type(native(bytes()))
<type 'str'>
>>> from future.utils import native_bytes
>>> type(native_bytes())
<type 'str'>

str/unicode

str/unicode の仕様変更も bytes に負けず劣らず凶悪なもの。

2.x

2.x では '' が文字列で u'' がユニコード文字列になった。

>>> type('a')
<type 'str'>
>>> type(u'a')
<type 'unicode'>

3.x

3.x では '' も u'' も型の名前は str だけど挙動は 2.x のユニコード文字列相当になっている。 それに併せて unicode 型が廃止された。

>>> type('a')
<class 'str'>
>>> type(u'a')
<class 'str'>
''

2.x/3.x

この問題も bytes と同様 builtins.str と future.unicode_literals をインポートしておくとバージョン間で挙動が一致して良い。

>>> from __future__ import unicode_literals
>>> from builtins import str
>>> type('a')
<type 'unicode'>
>>> type(u'a')
<type 'unicode'>
>>> type(str('a'))
<class 'future.types.newstr.newstr'>

型をチェックするときは 3.x で unicode というシンボルは使えないため future.utils.string_types を使うと良い。 この型は 2.x のときは unicode で 3.x のときは str と等価になる。 同様に test_type は 2.x のとき str または unicode (つまり basestring) で 3.x のとき str と等価になる。

>>> from future.utils import string_types, text_type
>>> isinstance('a', string_types)
True
>>> isinstance(u'a', string_types)
True
>>> isinstance(u'a', text_type)
True

math.ceil()/floor()

細かいけど 3.x では math.ceil()/floor() の返り値の型が変更された。

2.x

2.x では返り値が浮動小数点型になっている。

>>> from math import ceil, floor
>>> ceil(1.1)
2.0
>>> floor(1.1)
1.0

3.x

それに対して 3.x では整数型になった。

>>> from math import ceil, floor
>>> ceil(1.1)
2
>>> floor(1.1)
1

2.x/3.x

ちょい面倒だけど毎回 int にキャストしよう。

>>> from math import ceil, floor
>>> int(ceil(1.1))
2
>>> int(floor(1.1))
1

array

array.array()

これまた細かいけど array.array() の引数の型が変わった。

2.x

2.x ではユニコードでない方の文字列を引数に取る。

>>> import array
>>> array.array(b'b')
array('b')
>>> array.array(u'b')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: array() argument 1 must be char, not unicode
3.x

それに対して 3.x ではユニコードの方の文字列を取る。

>>> import array
>>> array.array(u'b')
array('b')
>>> array.array(b'b')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must be a unicode character, not bytes
2.x/3.x

これは基本的に u や b といった接頭辞を付けていなければ問題にはならない。

>>> import array
>>> array.array('b')
array('b')

ただ future.unicode_literals をインポートしていると 2.x でも '' がユニコード文字列になってしまう。 その場合はfuture.utils.bytes_to_native_str をバイト文字列と組み合わせるとよさげ。

>>> from __future__ import unicode_literals
>>> from future.utils import bytes_to_native_str
>>> import array
>>> array.array(bytes_to_native_str(b'b'))
array('b')

array.array#read()

3.x で廃止されたため代わりに array.array#fromfile() を使う。

>>> from array import array
>>> array.fromfile
<method 'fromfile' of 'array.array' objects>

base64

3.x

以下のメソッドは 3.1 以降に追加されたため 2.x では使えない。

>>> from base64 import decodebytes
>>> from base64 import decodestring
>>> from base64 import encodebytes
>>> from base64 import encodestring

2.x/3.x

2.x でも使うには future.standard_library.install_aliases() 関数を使ってバックポートを利用する。

>>> from future.standard_library import install_aliases
>>> install_aliases()
>>> from base64 import decodebytes
>>> from base64 import decodestring
>>> from base64 import encodebytes
>>> from base64 import encodestring

re

re モジュールのマッチングの挙動が 2.x ではバイト文字列、3.x ではユニコード文字列がデフォルトになっている。

2.x/3.x

3.x に挙動を寄せるためにフラグとして re.U を指定してユニコード文字列を使ったマッチングにするのが良いのかな。

>>> re.match('a', 'a', re.U)
<_sre.SRE_Match object at 0x107b30308>

struct.pack()

2.7.7 未満

struct.pack() は 2.7.7 未満はフォーマットにユニコード文字列が使えない。

>>> from struct import pack
>>> pack(u'!i', 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Struct() argument 1 must be string, not unicode

2.x/3.x

これは future.unicode_literals をインポートしている場合に問題となるので、フォーマットには b を付けて明示的にバイト列にしておいた方がいい。

>>> from __future__ import unicode_literals
>>> from struct import pack
>>> pack(b'!i', 1)
'\x00\x00\x00\x01'

辞書 (dict)

組み込みオブジェクトの辞書は API が一部変更されている。

2.x

2.x では list を返す dict#items() とイテレータを返す dict#iteritems() がある。

>>> type(dict().items())
<type 'list'>
>>> type(dict().iteritems())
<type 'dictionary-itemiterator'>

3.x

それに対して 3.x では dict#iteritems() が廃止されて代わりに dict#items() がイテレータを返すようになった。

>>> type(dict().items())
<class 'dict_items'>

2.x/3.x

2.x と 3.x の両方で動くようにするには常に dict#items() を使う。 ただし、2.x では list が返ることになるのでメモリのパフォーマンス面で懸念が生じる。 (list は中身のオブジェクトに必要なメモリを一度にアロケートする必要があるため) その場合は python-future が提供する builtins.dict をインポートすれば 3.x と同様の挙動になる。

>>> type(dict().items())
<type 'list'>
>>> from builtins import dict
>>> type(dict().items())
<type 'dict_items'>

ちなみにブラケットや辞書内方表記を使って辞書を作ると dict() を経由しないため python-future の builtins.dict をインポートしていてもオブジェクトが上書きされない点には注意が必要になる。 また、辞書内方表記はそもそも 2.7 以降で利用可能なシンタックスなので、2.6 でも動くようにするには dict() と辞書内方表記を組み合わせたイディオムを使うべし。

>>> from builtins import dict
>>> type({})
<type 'dict'>
>>> type({i: str(i) for i in range(1)})
<type 'dict'>
>>> type(dict([(i, str(i)) for i in range(1)]))
<class 'future.types.newdict.newdict'>

組み込み関数 range()/xrange()

3.x では 組み込み関数 xrange() が廃止された。

2.x

2.x には list を返す range() とイテレータを返す xrange() があった。

>>> type(range(1))
<type 'list'>
>>> type(xrange(1))
<type 'xrange'>

2.x/3.x

3.x では xrange() が廃止されたので、どちらでも動作するには常に range() を使う必要がある。

>>> type(range(1))
<class 'range'>

ただ、これも先ほどの dict#items() と同様、2.x はメモリのパフォーマンス面で懸念があるので python-future の builtins.range() をインポートしておくと良い。

>>> from builtins import range

組み込み関数 map() と itertools.imap()

3.x では itertools.imap() が廃止されて、組み込み関数 map() が返す型も変更されている。

2.x

2.x では組み込み関数 map() が list を返す。 また、itertools に map() のイテレータを返す版として imap() という関数があった。

>>> map(lambda x: x * 2, [1, 2, 3])
[2, 4, 6]
>>> from itertools import imap
>>> imap(lambda x: x * 2, [1, 2, 3])
<itertools.imap object at 0x10ba7c6d0>

2.x/3.x

3.x では itertools.imap() が廃止されたので map() を常に使う必要がある。 ただし、これについてもメモリのパフォーマンス面に懸念があるので builtins.map をインポートしておくと良い。

>>> map(lambda x: x * 2, [1, 2, 3])
[2, 4, 6]
>>> from builtins import map
>>> map(lambda x: x * 2, [1, 2, 3])
<map object at 0x10a923b38>

zip()/itertools.izip(), filter()/itertools.ifilter()

組み込み関数 map と itertools.imap と全く同じ問題が zip() と itertools.izip() および filter() と itertools.ifilter() にある。 対処方法は map()/itertools.imap() と同様。

組み込み関数 reduce()

組み込み関数 reduce は functiools.reduce() に移動した。

2.x

2.x では特に何も気にせず reduce() が使える。

>>> reduce(lambda x, y: x + y, [1, 2, 3])
6

2.x/3.x

3.x では functools から reduce をインポートする必要がある。 尚、2.x でも functools から reduce はインポートできるので、両方で動作させるには常にインポートしておけば良い。

>>> from functools import reduce
>>> reduce(lambda x, y: x + y, [1, 2, 3])
6

組み込み関数 raw_input/input()

ユーザから入力を求める組み込み関数は 2.x と 3.x で異なっている。

2.x

2.x ではユーザから入力を受け付けるには raw_input() を使う。

>>> print(raw_input('Enter: '))
Enter: Hello, World
Hello, World

3.x

それに対し 3.x では input() を使う。

>>> print(input('Enter: '))
Enter: Hello, World
Hello, World

2.x/3.x

2.x と 3.x の両方で動くようにするには builtins.input をインポートして使う。

>>> from builtins import input
>>> print(input('Enter: '))
Enter: Hello, World
Hello, World

2.x における組み込み関数 input()

ちなみに 2.x の組み込み関数 input() は別の意味があってユーザの入力内容を Python のコードとして扱う。

>>> input('Python: ')
Python: 1 + 1
2

それに対し 3.x の場合は input() が単にユーザの入力内容を受け付けるための関数になってしまっているため eval() で評価する必要がある。

>>> eval(input('Python: '))
Python: 1 + 1
2

2.x と 3.x の両方で動くようにしたい場合は builtins.input を使った上で eval() で評価する。

>>> from builtins import input
>>> eval(input('Python: '))
Python: 1 + 1
2

組み込み関数 open()/file()

組み込み関数 file() は 3.x で廃止されたので同等の動きをする前提として open() を使う。 さらに、組み込み関数 open() についても動作が変更されている点に注意が必要。

2.x

2.x ではオプションに 'b' を付けようと付けまいとファイルの内容はバイト文字列として返ってくる。

>>> type(open('greet.txt', 'r').readline())
<type 'str'>
>>> type(open('greet.txt', 'rb').readline())
<type 'str'>

3.x

それに対して 3.x では 'b' オプションが付いていないとユニコード文字列として返ってくる。

>>> type(open('greet.txt', 'r').readline())
<class 'str'>
>>> type(open('greet.txt', 'rb').readline())
<class 'bytes'>

2.x/3.x

python-future の builtins.open を使えば 2.x であっても 'b' を付けない場合にユニコード文字列が返ることになる。

>>> from builtins import open
>>> type(open('greet.txt', 'r').readline())
<type 'unicode'>
>>> type(open('greet.txt', 'rb').readline())
<type 'str'>

__str__ 特殊メソッド

オブジェクトの文字列表現を返す動作をオーバーライドするための str 特殊メソッドは 2.x と 3.x で想定する型が異なる。

2.x

2.x では型がバイト列の必要があるのでマルチバイト文字列はエンコードが必要になる。

>>> class MyClass(object):
...     def __str__(self):
...         return u'まいくらす'.encode('utf-8')
... 
>>> print(MyClass())
まいくらす

3.x

それに対し 3.x ではユニコードを想定している。

>>> class MyClass(object):
...     def __str__(self):
...         return u'まいくらす'
...
>>> print(MyClass())
まいくらす

2.x/3.x

両方のバージョンで動くようにするには future.utils.python_2_unicode_compatible デコレータでクラスを修飾すると良い。

>>> from future.utils import python_2_unicode_compatible
>>> @python_2_unicode_compatible
... class MyClass(object):
...     def __str__(self):
...         return u'まいくらす'
...
>>> print(MyClass())
まいくらす

イテレータプロトコル

ユーザクラスをイテレータを対応させるための仕様であるイテレータプロトコルも 2.x と 3.x でメソッド名が異なる。

2.x

2.x では次の要素を返すのに next() というメソッドを実装する。

>>> class Upper(object):
...     def __init__(self, iterable):
...         self._iter = iter(iterable)
...     def next(self):
...         return next(self._iter).upper()
...     def __iter__(self):
...         return self
...
>>> itr = Upper('hello')
>>> print(list(itr))
['H', 'E', 'L', 'L', 'O']

3.x

それに対し 3.x では __next__() という名前に変わっている。

>>> class Upper(object):
...     def __init__(self, iterable):
...         self._iter = iter(iterable)
...     def __next__(self):
...         return next(self._iter).upper()
...     def __iter__(self):
...         return self
...
>>> itr = Upper('hello')
>>> print(list(itr))
['H', 'E', 'L', 'L', 'O']

2.x/3.x

両方のバージョンで動くように作るには builtins.object をインポートして、それを継承する。

>>> from builtins import object
>>> class Upper(object):
...     def __init__(self, iterable):
...         self._iter = iter(iterable)
...     def __next__(self):
...         return next(self._iter).upper()
...     def __iter__(self):
...         return self
...
>>> itr = Upper('hello')
>>> print(list(itr))
['H', 'E', 'L', 'L', 'O']

あるいは単純に next() と __next__() の両方を実装してしまっても構わない。 この程度なら、こっちの方が楽かな。

>>> class Upper(object):
...     def __init__(self, iterable):
...         self._iter = iter(iterable)
...     def next(self):
...         return self.__next__()
...     def __next__(self):
...         return next(self._iter).upper()
...     def __iter__(self):
...         return self
...
>>> itr = Upper('hello')
>>> print(list(itr))
['H', 'E', 'L', 'L', 'O']

メタクラス

2.x と 3.x ではクラスにメタクラスを指定する方法が異なっている。

2.x

2.x ではクラスの __metaclass__ メンバにメタクラスを指定する。

from abc import ABCMeta
class MyClass(object):
    __metaclass__ = ABCMeta

3.x

3.x では通常は継承するクラスを指定する場所で metaclass 引数にメタクラスを指定する。

from abc import ABCMeta
class MyClass(metaclass=ABCMeta):
    pass

2.x/3.x

どちらのバージョンでも動くようにするには future.utils.with_metaclass を使ってメタクラスを追加する。

from abc import ABCMeta
from future.utils import with_metaclass
class MyClass(with_metaclass(ABCMeta)):
    pass

組み込み関数 execfile()

外部の Python スクリプトを実行する組み込み関数 execfile() は 3.x で廃止された。

試すには別途スクリプトが必要なのであらかじめ作っておく。

$ cat << EOF > sample.py
print('Hello, World!')
EOF

2.x

execfile() にファイル名を指定すると実行できる。

>>> execfile('sample.py')
Hello, World!

2.x/3.x

3.x では execfile() が廃止されたので代わりに以下のイディオムを使う。

>>> filename = 'sample.py'
>>> exec(compile(open(filename).read(), filename, 'exec'))
Hello, World!

組み込み関数 chr() と unichr()

3.x では unichr() が廃止された。

2.x

2.x では chr() がバイト文字列、unichr() がユニコード文字列を返していた。

>>> chr(97)
'a'
>>> unichr(97)
u'a'

3.x

3.x では unichr() が廃止されて、代わりに chr() がユニコード文字列を返すようになった。

>>> chr(97)
'a'

2.x/3.x

両方で動くようにするには builtins.chr をインポートして 2.x でも chr() がユニコード文字列を返すようにして、バイト文字列が欲しい場合には適宜エンコードする。

>>> from builtins import chr
>>> chr(97).encode('latin-1')
'a'
>>> chr(97)
u'a'

組み込み関数 cmp()

オブジェクト同士を比較する組み込み関数 cmp() は 3.x で廃止された。

2.x

2.x では cmp() が使える。

>>> cmp('a', 'b')
-1

2.x/3.x

3.x では cmp() が廃止されたので基本的に使わない方がいいけど、どうしても使いたい場合には等価な関数を作ることができる。

>>> cmp = lambda x, y: (x > y) - (x < y)
>>> cmp ('a', 'b')
-1

組み込み関数 reload() と imp.reload()

モジュールをインポートし直す組み込み関数 reload() は 3.x で imp パッケージ以下に移動した。

2.x

2.x において reload() は組み込み関数なので特に何もしなくても使える。

>>> import sample
Hello, World!
>>> reload(sample)
Hello, World!
<module 'sample' from 'sample.pyc'>

2.x/3.x

3.x において reload() は imp パッケージ以下に移動している。 これは 2.x でも利用できるため reload() を使う場合は常にインポートする。

>>> import sample
Hello, World!
>>> from imp import reload
>>> reload(sample)
Hello, World!
<module 'sample' from './sample.py'>

組み込み関数 intern() と sys.intern()

組み込み関数 reload() と同様に intern() も sys パッケージ以下に移動している。 ただ、こちらは 2.x では sys.intern でインポートできない。

2.x

2.x において intern() は組み込み関数。

>>> intern('Hello, World!')
'Hello, World!'

3.x

3.x では intern() が sys パッケージ以下に移動した。

>>> from sys import intern
>>> intern('Hello, World!')
'Hello, World!'

2.x/3.x

両方のバージョンで動かす場合は future.standard_library.install_aliases() を使って 2.x でもインポートできるようにする。

>>> from future.standard_library import install_aliases
>>> install_aliases()
>>> from sys import intern
>>> intern('Hello, World!')
'Hello, World!'

あるいは自分で対応する場合には ImportError を拾えば良い。

>>> try:
...     from sys import intern
... except ImportError:
...     pass
...
>>> intern('Hello, World!')
'Hello, World!'

New-Style クラスと Old-Style クラス

2.x にはユーザクラスを定義する際に書き方によって Old-Style クラスと New-Style クラスのふたつがあったけど 3.x では New-Style クラスに統一された。

New-Style クラスと Old-Style クラスではメンバに大きな違いがあるため、メタプログラミングをする場合に問題となる。 スクリプトを 2.x と 3.x の両方で動かす場合には常に New-Style クラスでユーザクラスを定義した方が良い。

>>> class MyClass:
...     pass
... 
>>> dir(MyClass)
['__doc__', '__module__']
>>> class MyClass(object):
...     pass
... 
>>> dir(MyClass)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

argparse

標準ライブラリへの追加は 2.7 以降

コマンドラインパーサの argparse は 2.7 以降から標準ライブラリに加わったため、それ以前のバージョンではパッケージマネージャを使ってインストールする必要がある。

$ pip install argparse

2.x と 3.x の挙動の違い

3.x の argparse には不具合があって、サブコマンドをパースする際に例外がそのまま出てしまう場合がある。 長いので詳しくは以下のブログ記事を参照のこと。

Python: argparse の挙動が Python 2.x と 3.x で異なる件

html

html 関連のライブラリは色々と整理されている。

cgi.escape()

この関数は 3.x で非推奨になった。

>>> from cgi import escape

代わりに html.escape() を使う。 これは 2.x では通常インポートできないが python-future をインストールしていれば使えるようになる。

>>> from html import escape

htmlentitydefs/html.entities

2.x の htmlentitydefs.* は整理されて 3.x で html.entities 以下に移動している。

>>> from htmlentitydefs import codepoint2name, entitydefs, name2codepoint

3.x では html.entities でインポートできる。 尚 python-future がインストールされていれば 2.x でも html.entities が利用できる。

>>> from html.entities import codepoint2name, entitydefs, name2codepoint

あるいは future.moves 経由で html.parse をインポートすることもできる。

>>> from future.moves.html.parser import HTMLParser

HTMLParser/html.parser

2.x の HTMLParser は html.parser に移動した。

>>> from HTMLParser import HTMLParser

3.x では html.parser でインポートできる。 python-future を使えば 2.x でも使える点は先ほどと同様。

>>> from html.parser import HTMLParser

http

http 関連のライブラリも 2.x と 3.x で統廃合が進んだ。

2.x

統廃合の対象になった 2.x のライブラリは次の通り。

>>> import httplib
>>> import Cookie
>>> import cookielib
>>> import BaseHTTPServer
>>> import SimpleHTTPServer
>>> import CGIHTTPServer

2.x/3.x

3.x では http.* にまとめられた。 ちなみに python-future がインストールされていれば以下の名前でインポートできる。

>>> import http.client
>>> import http.cookies
>>> import http.cookiejar
>>> import http.server

xmlrpclib,etc/xmlrpc

XML-RPC 関連のライブラリは xmlrpc.* 以下に統廃合された。

2.x の XML-RPC サーバはトップレベルのライブラリだった。

>>> import DocXMLRPCServer
>>> import SimpleXMLRPCServer

3.x では xmlrpc.server に移動した。

>>> from xmlrpc import server

同様に 2.x では xmlrpclib もトップレベルのライブラリだった。

>>> xmlrpclib

3.x では xmlrpc.client に移動している。

>>> xmlrpc.client

tkinter/Tkinter

2.x で Tkinter 関連のライブラリはトップレベルで雑多に用意されていた。

>>> import Tkinter
>>> import Dialog
>>> import FileDialog
>>> import ScrolledText
>>> import SimpleDialog
>>> import Tix
>>> import Tkconstants
>>> import Tkdnd
>>> import tkColorChooser
>>> import tkCommonDialog
>>> import tkFileDialog
>>> import tkFont
>>> import tkMessageBox
>>> import tkSimpleDialo

3.x では tkinter.* 以下に整理されている。

>>> import tkinter
>>> import tkinter.dialog
>>> import tkinter.filedialog
>>> import tkinter.scrolledtext
>>> import tkinter.simpledialog
>>> import tkinter.tix
>>> import tkinter.constants
>>> import tkinter.dnd
>>> import tkinter.colorchooser
>>> import tkinter.commondialog
>>> import tkinter.filedialog
>>> import tkinter.font
>>> import tkinter.messagebox
>>> import tkinter.simpledialog

StringIO/io

StringIO はユニコード文字列を扱うものとバイト文字列を扱うもので分割された上で io パッケージ以下に移動された。

2.x

2.x では StringIO パッケージでインポートしていた。

>>> from StringIO import StringIO

2.x/3.x

3.x ではユニコード文字列を扱う StringIO とバイト文字列を扱う BytesIO に分かれて io パッケージ以下に移動した。 ちなみにこれは 2.x でもインポートできるので、どちらでも動くようにするには常に io パッケージを使うようにする。

>>> from io import StringIO
>>> from io import BytesIO

UserDict/UserList/UserString

User* は itertools 以下に移動した。

2.x

2.x ではトップレベルのパッケージとして User* があった。

>>> from UserDict import UserDict
>>> from UserList import UserList
>>> from UserString import UserString

3.x

3.x では collections 以下でインポートできる。

>>> from collections import UserDict, UserList, UserString

2.x/3.x

2.x でも collections 以下でインポートできるようにするには future.standard_library.install_aliases() を使う。

>>> from future.standard_library import install_aliases
>>> install_aliases()
>>> from collections import UserDict, UserList, UserString

または future.moves 以下の collections でインポートしても良い。

>>> from future.moves.collections import UserDict, UserList, UserString

url*

URL 関連のパッケージは 2.x で雑多にトップレベルのパッケージがあた状態から 3.x で url.* に整理された。

2.x

2.x では urlparse や urllib などがトップレベルのパッケージとしてあった。

>>> from urlparse import urlparse
>>> from urllib import urlencode
>>> from urllib2 import urlopen, Request, HTTPError

3.x

3.x では urllib.* 以下に整理された。

>>> from urllib.parse import urlparse, urlencode
>>> from urllib.request import urlopen, Request
>>> from urllib.error import HTTPError

2.x/3.x

2.x でも urllib.* 以下でインポートできるように future.standard_library.install_aliases() を使う。

>>> from future.standard_library import install_aliases
>>> install_aliases()
>>> from urllib.parse import urlparse, urlencode
>>> from urllib.request import urlopen, Request
>>> from urllib.error import HTTPError

または future.moves 経由でインポートする。

>>> from future.moves.urllib.parse import urlparse, urlencode
>>> from future.moves.urllib.request import urlopen, Request
>>> from future.moves.urllib.error import HTTPError

itertools

itertools はバージョン間で差異が多くてしんどい。

ifilterfalse()/filterfalse(), izip_longest()/zip_longest()

ifilterfalse と izip_longest は 3.x で filterfalse と zip_longest に名前が変更された。

両方のバージョンで動くようにするには future.standard_library.install_aliases() を使う。

>>> from future.standard_library import install_aliases
>>> install_aliases()
>>> from itertools import filterfalse, zip_longest

または future.moves 経由でインポートする。

>>> from future.moves.itertools import filterfalse, zip_longest

対処方法は以降の変更点も同じはず。

count()

count() には 2.7/3.1 以降に step パラメータ追加された。

compress()/combinations_with_replacement()

compress と combinations_with_replacement は 2.7/3.1 以降に追加された。

accumulate

accumulate は 3.2 以降に追加された。

commands/subprocess/etc

Python で外部のコマンドを実行する手段は色々とあるけど今後は subprocess を使うのが推奨されている。 subprocess 以外は基本的に使わないこと。

  • subprocess (推奨)
  • commands (3.x で廃止)
  • popen2 (3.x で廃止)
  • os.system
  • os.spawn*
  • os.popen

check_output()

2.7+

subprocess の check_output() は 2.7 以降で追加された。

>>> from subprocess import check_output
2.x/3.x

全てのバージョンで使えるようにするには future.standard_library.install_aliases() を使う。

>>> from future.standard_library import install_aliases
>>> install_aliases()
>>> from subprocess import check_output

または future.moves 経由でインポートする。

>>> from future.moves.subprocess import check_output

repr/reprlib

2.x の repr は 3.x で reprlib に名前が変わった。

3.x

recursive_repr は 3.x にしかない。

from reprlib import recursive_repr

2.x/3.x

2.x でも使えるようにするには future.standard_library.install_aliases() を使う。

from future.standard_library import install_aliases
install_aliases()
from reprlib import recursive_repr

dbm

dbm 関連のモジュールは 3.x で整理された。

2.x

2.x ではトップレベルのパッケージとして雑多に用意されていた。

>>> import anydbm
>>> import whichdb
>>> import dbm
>>> import dumbdbm
>>> import gdbm

2.x/3.x

3.x では anydbm と whichdb が dbm 以下に移動した。 また、gdbm は gnu に名前が変更された上で dbm 以下に移動している。

2.x でも 3.x と同じ名前で使うには future.standard_library.install_aliases() を使う。

>>> from future.standard_library import install_aliases
>>> install_aliases()
>>> import dbm
>>> import dbm.ndbm
>>> import dbm.dumb
>>> import dbm.gnu

あるいは future.moves 経由でインポートする。

>>> from future.moves import dbm
>>> from future.moves.dbm import dumb
>>> from future.moves.dbm import ndbm
>>> from future.moves.dbm import gnu

collections

collections もバージョン間で差異がそこそこある。

2.7+

Counter/OrderedDict は 2.7 以降で追加された。

>>> from collections import Counter, OrderedDict

3.3+

ChainMap は 3.3 以降で追加された。

>>> from collections import ChainMap

2.x/3.x

すべてのバージョンで使えるようにするには future.standard_library.install_aliases() を使うか future.moves 経由でインポートする。

>>> from future.standard_library import install_aliases
>>> install_aliases()
>>> from collections import Counter, OrderedDict, ChainMap
>>> from future.moves.collections import Counter, OrderedDict, ChainMap

バックポート

いくつかのパッケージはバージョンの途中で標準ライブラリに追加された。 追加以前の過去のバージョンではサードパーティ製のパッケージとしてインストールできる。

例えば 2.7 で追加された argparse を 2.6 で利用するには同名のバックポートをインストールした上で利用する。

$ pip install argparse
追加されたバージョン パッケージ名 バックポート名
3.4 enum enum34
3.4 singledispatch singledispatch
3.4 pathlib pathlib
3.3 lzma backports.lzma
2.7 argparse argparse
2.7 importlib importlib
2.7 unittest2 unittest2

functools.lru_cache

functools.lru_cache は 3.2 で追加された。

>>> from functools import lru_cache

バックポートは functools32 という名前で提供されている。 2.x と 3.x の両方で動くようにするにはインストールした上で以下のようにする。

try
    from functools import lru_cache
except ImportError:
    from functools32 import lru_cache

リネームされた標準ライブラリ

標準ライブラリには 2.x と 3.x で名前が異なるものがある。 これは PEP8 の推奨しているモジュール名に適合させるためのものが多そう。

Queue/queue

2.x

2.x ではパッケージ名が Queue だった。

>>> import Queue
3.x

3.x では queue に変更になっている。

>>> import queue
2.x/3.x

愚直にやる場合は ImportError をキャッチしてインポートし直す。

>>> try:
...     import Queue as queue
... except ImportError:
...     import queue
...

もっと賢くやる場合は python-future を使う。 python-future がインストールされている場合には 2.x でも queue の名前が使える。

>>> import queue

ConfigParser/configparser

同様に 2.x の ConfigParser は 3.x では configparser にリネームされている。

SocketServer/socketserver

2.x の SocketServer は 3.x で socketserver になった。

copy_reg/copyreg

2.x の copy_reg は copyreg になった。

_winreg/winreg

2.x の _winreg は winreg になった。

dummy_threading/_dummy_thread

2.x の dummy_threading は _dummy_thread になった。

markupbase/_markupbase

2.x の markupbase は 3.x の _markupbase になった。

thread/_thread

2.x の thread は 3.x の _thread になった。

まとめ

この記事では、まず始めに最近の Python の状況についてまとめた。 残念ながら、一部の Pythonista はあと 10 年ほどは 2.x と付き合いを続けていくしかない。 その上で、どのように付き合っていくか考えるためにバージョン間の差異についてまとめてみた。 そして、困難な 2.x と 3.x の違いの吸収を python-future を使って行う方法についても書いた。

参考