【Python2to3アップデート方法】Python2系と3系の違い

## 目次
- [【Python2to3アップデート方法】目次](https://qiita.com/Esfahan/items/067484ee0cb3dd374992)

## 概要
Python2系から3系で、互換性の無い部分について、特に気をつけなければいけない箇所についてまとめた。
ここでは、versionによる違いを、それぞれ以下の3通りの書き方と実行結果を用いて説明する。
また、2.6以降を利用していることを前提とする。

- 2系での書き方、実行結果
- 3系での書き方、実行結果
- 2系で\_\_future\_\_モジュールを使った書き方、実行結果

#### \_\_future\_\_モジュールとは
Python2系にデフォルトで入っており、Python3で実装された互換性のない機能をPython2系で利用できるようにするモジュール。つまり、Python2系で、3系の書き方をできるようにするモジュール。

Python2系から3系に移行する際に、一時的に2系でも3系でも動くコードにしたい場合の一時対策として、必要であれば利用するのが望ましい。


https://qiita.com/amedama/items/5e5a09b3a88dfd48198e
> __future__はPython2.1で導入された比較的古いモジュールで、Pythonの旧バージョンで新バージョンの挙動を一部先に取り込みたいときに使われてきた。現在ではPython 2系と3系の挙動を近づけるために言及されることが多いが、必ずしも3系対応のために存在するのではない。

## 2系と3系との違い
1. [printについて (print_function)](#1-printについて)
2. [str型とunicode型について (unicode_literals)](#2-str型とunicode型について)
    1. [よく遭遇するエラー](#よく遭遇するエラー)
3. [割り算の少数点以下の扱いについて (division)](#3-割り算の少数点以下の扱いについて)
4. [import方法について (absolute_import)](#4-import方法について)
5. [その他](#その他)

## 1. printについて
Python3系では`print`は関数になり、予約語からも削除された。

### 2系
2系での書き方。

```py
print 'version2'
```

### 3系
3系での書き方。

```py
# 3系では以下にする
print('version3')
```

### 2系で\_\_future\_\_モジュール
Python2系で3系の実装をする場合の書き方。
（printは、2系でもmoduleをimportしなくても3系の書き方が可能だが）

```py
from __future__ import print_function
print("Message")
```

## 2. str型とunicode型について
2系では、文字列型はstrとunicodeの2つがあったが、3系ではstrのみとなり、またstrとはunicodeの意味となった。つまり、2系と3系ではstr型の意味が違う。

### 2系
以下のコードのif文は、strとunicodeを比較することになるので、エラーとなる。

```py
# -*- coding:utf-8 -*-
message = 'サンプル文字列'
print(type(message))

if 'あ' == u'\u3042':
    print('True')
```

#### 実行結果

```
<type 'str'>
sample.py:5: UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
  if 'あ' == u'\u3042':
```

### 3系
以下のコードはstr(unicode)同士の比較なのでエラーにならない。
また、Python3ではデフォルトのcodingがutf-8になっているため、`# -*- coding:utf-8 -*-`を書く必要はない。（あっても問題ない）

```py
message = 'サンプル文字列'
print(type(message))

if 'あ' == u'\u3042':
    print('True')
```

#### 実行結果
`<type 'str'>` 3系でこれはunicodeの意味。

```
<type 'str'>
True
```

### よく遭遇するエラー
2系から3系の間では、このエラーに遭遇することがよくある。

```
AttributeError: 'str' object has no attribute 'decode'"
```

もしくは

```
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 0: ordinal not in range(128)
```

これも、前述のstr型が2系と3系で違うことが原因。
以下のプログラムを2系、3系それぞれで実行してみると、以下のようになる。

```py
# -*- coding:utf-8 -*-
raw = u'サンプル' # python3系では接頭辞uはなくてもunicodeになるので不要。合っても問題はないが。
print(type(raw))

text = raw.encode('utf-8')
print(type(text))

decoded = text.decode('utf-8')
print(type(decoded))
```

2系の結果

```
<type 'unicode'>
<type 'str'>
<type 'unicode'>
```

3系の結果

```
<type 'str'>
<type 'bytes'>
<type 'str'>
```

2系ではstr型をencodeするとunicode型になるが、3系でstr型をencodeするとbytes型になる。

#### エラーが発生する原因
3系でstrをdecodeしようとすると、以下のエラーが出る。encodeは可能。

```
AttributeError: 'str' object has no attribute 'decode'"
```

2系でstrをencodeしようとすると、以下のエラーが出る。decodeは可能。

```
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 0: ordinal not in range(128)
```

つまり、str型をdecodeする記述のあるプログラムは、2系では動くが3系では動かない。また逆に、str型をencodeする記述は、3系では動くが2系では動かない。それぞれ環境に合わせて修正する必要がある。

### 2系で\_\_future\_\_モジュール
Python2系で3系の実装をする場合。
以下のコードはunicode同士の比較なのでエラーにならない。

unicode_literalsモジュールは、prefix無しの文字列リテラルをunicodeにする。
以下のコードの`'あ'`は、`u'あ'`と同義になる。
また、こちらでは`# -*- coding:utf-8 -*-`が必要。

```py
# -*- coding:utf-8 -*-
from __future__ import unicode_literals
message = 'サンプル文字列'
print(type(message))

if 'あ' == u'\u3042':
    print('True')
```

#### 実行結果
2系で実行すると`<type 'unicode'>`となる。
3系で実行すると`<type 'str'>`となる。

```
<type 'unicode'>
True
```

## 3. 割り算の少数点以下の扱いについて
2系では、int同士の割り算は小数点以下切り捨てだったが、3系ではfloatになる。
3系で切り捨てにしたい場合は、演算子「//」 を使う。

### 2系

```py
print(1/2)
print(1//2)
```

#### 実行結果

```
0
0
```

### 3系

```py
print(1/2)
print(1//2)
```

#### 実行結果

```
0.5
0
```

### 2系で\_\_future\_\_モジュール
Python2系で3系の実装をする場合。

```py
from __future__ import division
print(1/2)
print(1//2)
```

#### 実行結果

```
0.5
0
```


## 4. import方法について
2系と3系ではimportの方法に違いがある。
2系では、カレントディレクトリのモジュールが優先されるが、3系では相対より絶対インポートが優先となる。これは、違うディレクトリに同名のファイルがある場合に問題となり得る。

実際に例を見たほうが早いので、以下に例を挙げる。

### ディレクトリ構成
以下の構成でPythonスクリプトを作成する。

greeting.pyとlibs/greeting.pyという同名の2つのファイルがある。
libs/greeting.pyをlibs/sample_lib.pyから`import greeting`として読み込んだときに、2系ではlibs/greeting.pyがimportされるが、3系では一番上の階層のgreeting.pyがimportされる。
先述したとおり、3系のimportでは相対パスより絶対パスが優先されるためだ。

```shell-session
.
|-- greeting.py
`-- libs
    |-- __init__.py
    |-- greeting.py
    `-- sample_lib.py
```

### スクリプトの中身
#### greeting.py

```py:greeting.py
from libs import sample_lib
```

#### libs/\_\_init\_\_.py
空で作成する。

#### libs/greeting.py

```py:greeting.py
def hello():
    return 'Hello'
```

#### libs/sample_lib.py

```py:sample_lib.py
import greeting

print(greeting.hello())
```

### 2系
top階層のgreeting.pyを実行する。
libs/sample_lib.pyでlibs/greeting.pyがimportされる。

```shell-session
$ python greeting.py
Hello
```

### 3系
一番上の階層のgreeting.pyを実行する。
3系では、libs/sample_lib.pyで一番上の階層のgreeting.pyをimportしようとし、`hello()`メソッドがないためエラーとなる。

```shell-session
$ python greeting.py
Traceback (most recent call last):
  File "greeting.py", line 1, in <module>
    from libs import sample_lib
  File "/home/username/libs/sample_lib.py", line 3, in <module>
    print(greeting.hello())
AttributeError: module 'greeting' has no attribute 'hello'
```

#### 修正方法
libs/sample_lib.pyを以下の用に修正する。

```diff:libs/sample_lib.py
-import greeting
+from . import greeting

print(greeting.hello())
```

これで3系でエラーが出なくなる。

```shell-session
$ python greeting.py
Hello
```

### 2系で\_\_future\_\_モジュール
libs/sample_lib.pyでabsolute_importをimportし、以下のように修正する。
（printを使っているのでprint_functionもimportする）

```diff:libs/sample_lib.py
-import greeting
+from __future__ import print_function
+from __future__ import absolute_import
+from . import greeting

 print(greeting.hello())
```

これでエラーが出なくなる。

```shell-session
$ python greeting.py
Hello
```

## その他
以下は、2to3やfutureで検出されない2系と3系の違い。

### tempfile
2系と3系では、tempfileモジュールのdefaultの書き込みモードが違う。

| ver. | mode |
| --: | --- |
| 2 | テキスト |
| 3 | バイナリ |

Python3系で、2系同様にテキストを書き込もうとすると、以下のエラーが出る。
`TypeError: a bytes-like object is required, not 'str'`

3系でテキストモードで書き込む場合は、以下の様にオプションを指定する必要がある。

```py
# -*- coding:utf-8 -*-
import tempfile

with tempfile.NamedTemporaryFile(mode='w+t', encoding='utf-8', delete=False) as tf:
    tf.write('テキスト')
```

参考：[python3 の tempfile + write はデフォルトのままだと <object>.write(<a bytes-like object>) が求められる](https://qiita.com/nueko/items/8804f2ecee7c3f88cd62)

### byte型とstring型
以下は2系のソースを3系になおすときによく使用した。

参考：[Python 3 での文字列とバイト列の相互変換と16進数表示](https://qiita.com/masakielastic/items/2a04aee632c62536f82c)

```py
# 文字列をbyte型に変換
'abcd'.encode()
# byte型を文字列に変換
b'abcd'.decode()
```

## 参考
- [Python 2 から Python 3 への移植 - docs.python.org](https://docs.python.org/ja/3/howto/pyporting.html)
- [2系を使いながらも3系のノリでコーディングする方法!!](https://kiwamiden.com/about-the-__future__-module)
- [\_\_future\_\_ モジュールについて](http://salinger.github.io/python/20140211-future-module/)
- [Python 2.7.x と 3.x の決定的な違いを例とともに](https://postd.cc/the-key-differences-between-python-2-7-x-and-python-3-x-with-examples/)