2017年7月12日水曜日

Python3移行の注意点 ZIP・MAP組み込み関数

Python2のサポートは2020年までだそうです。Python3に関しては私は殆ど使用してこなかったのですが、最近環境も揃ってきたような気もします。このタイミングで、プラグイン・ライブラリのPython3への移行作業してみました。

作業自体は一日程度で終わりました。また移行したライブラリのPython2.7での動作は何も問題が無く、後方互換性もバッチリで感心しました。

今回はPython3向け修正作業で感じた、ZIP・MAP組み込み関数の注意点について記事にします。

ZIP関数、Python2での動作
まずPython2での動作を見てみます。
>>> zip([1,2,3],[4,5,6],[7,8,9])
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
3つのリストを一つのリストに変換しています。

また、For文やリスト内包表記を使ってみます。

>>> z = zip([1,2,3],[4,5,6],[7,8,9])
>>> for i, j, k in z:
...     print i, j, k
1 4 7
2 5 8
3 6 9
>>> [(i, j+k) for i, j, k in z]
[(1, 11), (2, 13), (3, 15)]
とくに問題なく、動作します。
ZIP関数、Python3での動作
同様にZIP関数をPython3で動作させてみます。
>>> zip([1,2,3],[4,5,6],[7,8,9])
<zip object at 0x10d6968c8>
なんと、zipオブジェクトが返ってきました。

For文やリスト内包表記を試してみます。

>>> z = zip([1,2,3], [4,5,6], [7,8,9])
>>> for i, j, k in z:
...     print(i, j, k)
1 4 7
2 5 8
3 6 9
>>> [(i, j+k) for i, j, k in z]
[]

For文は実行できましたが、リスト内包表記は空のリストが返ってきただけでした。

zipオブジェクトというのは、ジェネレータと同様の遅延評価の機能を持っているそうですが、複数回の利用はできないみたいです。

ZIP関数をPython2と同様の動作に記述するには
Python3で動作させるには、工夫が必要です。3つ案があります。

【案1】同じ記述を2つする

>>> for i, j, k in zip([1,2,3], [4,5,6], [7,8,9]):
...     print(i, j, k)
1 4 7
2 5 8
3 6 9
>>> [(i, j+k) for i, j, k in zip([1,2,3], [4,5,6], [7,8,9])]
[(1, 11), (2, 13), (3, 15)]
zipオブジェクトは遅延評価なので、2つ記述してもオーバヘッドは無いようです。

【案2】リストやタプルに変換する

>>> z = list(zip([1,2,3], [4,5,6], [7,8,9]))
>>> for i, j, k in z:
...     print(i, j, k)
1 4 7
2 5 8
3 6 9
>>> [(i, j+k) for i, j, k in z]
[(1, 11), (2, 13), (3, 15)]

【案3】itertools.teeを使用

>>> import itertools
>>> z = zip([1,2,3], [4,5,6], [7,8,9])
>>> z1, z2 = itertools.tee(z)
>>> for i, j, k in z1:
...     print(i, j, k)
1 4 7
2 5 8
3 6 9
>>> [(i, j+k) for i, j, k in z2]
[(1, 11), (2, 13), (3, 15)]
>>>
itertools.teeで、一つのイテレータからN個の独立したイテレータを返すことができます。
参考: itertools.tee(iterable, n=2)

他にもあるかもしれませんが、取り敢えず、これだけ挙げてみました。

MAP関数、Python3での動作

Python3では、MAP関数もZIPと同様の動作になります。

>>> map(lambda i,j,k: (i, j+k) , [1,2,3], [4,5,6], [7,8,9])
<map object at 0x10d69a2e8>
>>> z = map(lambda i,j,k: (i, j+k) , [1,2,3], [4,5,6], [7,8,9])
>>> for i in z:
...     print(i)
(1, 11)
(2, 13)
(3, 15)
>>> [i for i in z]
[]

また、Python2 では最初の引数に None を指定すると、最大の配列数に長さを合わせて結合してくれました。しかし、Python3では使用するとエラーになります。

Python2

>>> map(None, [1,2,3], [4,5], [7])
[(1, 4, 7), (2, 5, None), (3, None, None)]

Python3

>>> list(map(None, [1,2,3], [4,5], [7]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
MAP関数をPython2と同様の動作に記述するには

MAP関数も、ZIP同様の解決策を適用可能です。

【案1】同じ記述を2つする

>>> for i in map(lambda i,j,k: (i, j+k) , [1,2,3], [4,5,6], [7,8,9]):
...     print(i)
(1, 11)
(2, 13)
(3, 15)
>>> [i for i in map(lambda i,j,k: (i, j+k) , [1,2,3], [4,5,6], [7,8,9])]
[(1, 11), (2, 13), (3, 15)]

【案2】リストやタプルに変換する

>>> z = list(map(lambda i,j,k: (i, j+k) , [1,2,3], [4,5,6], [7,8,9]))
>>> for i in z:
...     print(i)
(1, 11)
(2, 13)
(3, 15)
>>> [i for i in z]
[(1, 11), (2, 13), (3, 15)]

【案3】itertools.teeを使用

>>> import itertools
>>> z = map(lambda i,j,k: (i, j+k) , [1,2,3], [4,5,6], [7,8,9])
>>> z1, z2 = itertools.tee(z)
>>> for i in z1:
...     print(i) 
(1, 11)
(2, 13)
(3, 15)
>>> [i for i in z2]
[(1, 11), (2, 13), (3, 15)]
参考: itertools.tee(iterable, n=2)

【案4】 itertools.zip_longestを使用
Python2の map(None, [1,2,3], [4,5], [7]) と同じ動作をさせる方法ですが、 itertools.zip_longest を使うと良いそうです。

>>> import itertools
>>> list(itertools.zip_longest([1,2,3], [4,5], [7]))
[(1, 4, 7), (2, 5, None), (3, None, None)]
itertools.zip_longest も、ZIP関数同様の遅延評価のイテレータです。
参考:itertools.zip_longest
最後に
ZIPやMAP関数の動作の違いは、Python2のコードをPython3で動かしたらエラーになる訳ではないので、発覚しにくいです。仕様の違いを知らないと悩むことになるので、気を付けたいですよね。

参考
Cannot unpack a zip object multiple times
Python 3 vs Python 2 map behavior