こんな人にオススメ
python3系でzip
関数を定義、使用した後に再度使用したら中身が空なんだが、なぜ?
ということで、今回はzip
関数の中身が空になる問題について解説する。これは実際に執筆者本人が体験したことだ。具体的には以下のコードを作成した際に、pythonのバージョンで挙動が異なる。
a = [1, 2, 3] b = (10, 2, 30) z = zip(a, b) for i in range(3): print('loop{}'.format(i + 1)) for j in z: print(j)
このコードではa
でlist
を、b
でtuple
を定義している。そしてa
, b
をz
にzip
としてまとめた。このz
のそれぞれの要素をj
のループで取り出す、という操作をi
のループで合計3回行なっている。
python2系での挙動は以下。ループごとに要素が取り出されている。
# loop1 # (1, 10) # (2, 2) # (3, 30) # loop2 # (1, 10) # (2, 2) # (3, 30) # loop3 # (1, 10) # (2, 2) # (3, 30)
一方でpython3系での挙動は以下。第2, 3ループで要素が取り出されなくなっている。
# loop1 # (1, 10) # (2, 2) # (3, 30) # loop2 # loop3
要素が取り出されなくなっていることで、自身が行いたい操作ができなかったのでいろいろ調べて本記事でまとめる。
python環境は以下。今回はpython2系と3系を使用する。
- Python 2.7.16
- Python 3.9.4
- numpy 1.20.3
zip
の出力の確認
まずは現状確認として、2系と3系での変数の出力を行う。
2系ではlist
、3系ではzip
内容出力は変数として作成したa
, b
, z
とそのtype
。コードは以下。
# coding: UTF-8 import platform # pythonのバージョンを出力 version = platform.python_version() a = [1, 2, 3] b = (10, 2, 30) z = zip(a, b) # zipは2系ではlist、3系ではzip result = [ 'version: {}'.format(version), 'a: {}'.format(str(a)), 'type(a): {}'.format(str(type(a))), 'b: {}'.format(str(b)), 'type(b): {}'.format(str(type(b))), 'z: {}'.format(str(z)), 'type(z): {}'.format(str(type(z))), 'list(z): {}'.format(str(list(z))), 'type(list(z)): {}'.format(str(type(list(z)))), ] with open('zip_behavior{}.txt'.format(version[0]), mode='w') as f: # 改行は指定しないと入らない f.write("\\n".join(result))
print
で出力しない理由は、バージョンによるprint
の違いだ。
- 2系:print文
- 3系:print関数
具体的には以下の感じ。
- 2系:
print a
- 3系:
print(a)
try
文でpythonのバージョンごとに出力を変えようとしたが、実行時のSyntaxError
で引っ掛かるのでやむを得ずファイルへと出力した。さらによく使用するf-string
もpython3.6からの機能なので今回は.format
で文字列の結合を行なっている。
ファイルの中身は以下。まずはpython2系。
version: 2.7.16 a: [1, 2, 3] type(a): <type 'list'> b: (10, 2, 30) type(b): <type 'tuple'> z: [(1, 10), (2, 2), (3, 30)] type(z): <type 'list'> list(z): [(1, 10), (2, 2), (3, 30)] type(list(z)): <type 'list'>
続いて3系。
version: 3.9.4 a: [1, 2, 3] type(a): <class 'list'> b: (10, 2, 30) type(b): <class 'tuple'> z: <zip object at 0x1013fda40> type(z): <class 'zip'> list(z): [(1, 10), (2, 2), (3, 30)] type(list(z)): <class 'list'>
a
やb
のtype
がtype
かclass
かの表記の違いがあるが今回はここはスルー。注目いただきたいのは中央付近のz
の出力。
- 2系:
list
- 3系:
zip
として出力されている。そして、3系ではlist
に変換すると中身を確認することが出来るようだ。
python3のzip
はイテレータ
色々と調べてみるとどうやら3系でのzip
は「イテレータ」と呼ばれるものらしい。参考にしたサイトについては最後に記載する。
イテレータとは
そもそもイテレータ(iterator)とはなんなのかということだが、ざっくりまとめると以下のようだ。
- イテラブルなオブジェクトの中の1つ
- 要素を取り出す度にどこまで取り出したのかを記憶(保持)する
- ただし、取り出した要素は取り出すごとに消滅する
なるほど、取り出すごとに要素が削除されるのか。だから、2回目以降のループでは中身が消えたのか。
しかし、イテラブルとは。ということで次でイテレータとイテラブルについて解説する。
イテレータとイテラブル
イテレータと似た言葉で「イテラブル」というものがある。イテラブルはざっくりまとめると以下のようだ。
- 要素に繰り返しアクセスして取得できるもの(オブジェクト)
- すなわち繰り返して使用可能なもの(オブジェクト)
for i in ...
の「...
」におけるものlist
,tuple
,range
などがイテラブル- ただし、要素がどこまで取り出したのかは保持しない
ふむふむ。繰り返して使用できるものがイテラブルということか。その中でも、取り出した順番を保持しつつ、取り出すごとに要素を消すのがイテレータ。
分類分けとしては以下の感じ。他にも該当するものがあるが、代表的なものを記載。
- not イテレータ(イテラブル、取り出し場所の保持ナシ、使用しても要素は消えない)
- 文字列
list
tuple
range
dict
やdict.keys()
,dict.values()
,dict.items()
set
- イテレータ(イテラブル、取り出し場所の保持アリ、使用したら要素は消える)
iter(list)
とか、 not イテレータをiter
したもの- ジェネレータ
「ジェネレータ」。新しい単語が出てきたので次章で説明する。また、iter
は次節で例を示す。
iter
とnext
前節でiter
なるものが出てきたが、これは「イテラブル」を「イテレータ」にするための関数。そして、イテレータの特徴として以下のものがあった。
- 要素をどこまで取り出したのかを保持
- 取り出し次第その要素を削除
iter
関数を使用した時に実際にイテレータとなるかどうかを確かめるに以下のコードを作成した。実際にイテレータになっていることがわかる。
また、要素の保持と削除を確かめるために以下のコードを作成した。ここでnext
なる関数を使用する。これはイテレータの要素を順番に出力することが出来るというもの。例えば以下のような例。
a
, b
の要素数は3なので1, 2, 3回目のnext
は正常に動く。途中に他の出力が入っても大丈夫。
# z: <zip object at 0x101513880> # 1回目のnext: (1, 10) # 2回目のnext: (2, 2) # 途中に文字列を挟む # 3回目のnext: (3, 30)
しかし、4回目のnext
をするとStopIteration
というエラーが発生する。これは、次に取り出す要素が無くなったから。
# print('4回目のnext: {}'.format(next(z))) # StopIteration
そして、取り出し次第その要素を削除という点に関しては以下のコードで確認することができる。list
にして中身を確認すると、実際に中身が消えていることが確認できる。
なお、z
を再定義するという点については後ほど述べる。
この例だと2回next
をしているので前2つの要素が削除され、list(z)
が最後の3
と30
だけになっている。
イテレータとイテラブルまとめ
執筆者的なイテレータとイテラブルのまとめ。
- イテラブルの中にイテレータという分類がある
- イテラブル自体の意味は繰り返し使用可能なもの(オブジェクト)
- その中でも以下の特徴を持つのがイテレータ
- 要素をどこまで取り出したのかを保持
- 取り出し次第その要素を削除
イテレータとジェネレータ
さて、前章で「ジェネレータ」という言葉が出てきた。イテレータにジェネレータ。似たような言葉だが、意味合いとしては並列ではないようだ。
あくまでも「イテレータを作成するための関数」のようだ。
ジェネレータとは
上でも書いたように、ジェネレータとは「イテレータを作成するための関数」。その他の説明としては、「要素を取り出したいときにその都度取り出せる関数」という感じ。
ジェネレータとイテレータの違い
イテレータがlist
とかの既に形の決まっているものであるなら、ジェネレータはdef ...():
のように決まった形のないもの、というイメージだろうか。どちらも配列として機能させることはできるが、より自由度が高いのがジェネレータ?か?なのか?
ただ、明確に異なるのが以下。
return
:return
を使って一括で要素を取り出すgenerator
:yield
で要素ごとに取り出す
yield
は次節で例を示すが、next
と同じような役割。
ジェネレータのメリット
ジェネレータのメリットは要素ごとに取り出すことが出来るという点。ということは、大量のデータを順番に出力する際に
return
: 一旦、全てのデータを作成し、必要な部分を取り出すgenerator
: 必要な時に必要な分だけ取り出す
という違いが効いて、実行時間に大きく差が出る。例えば以下のコード。100×100のデータを500,000回作成し、必要なインデックスだけ出力するというもの。
import numpy as np import time
まずはreturn
を使用した場合。
実行時間は7秒程度。一方でgenerator
を使用すると、実行時間は0.001秒程度と5,000倍以上も早いという結果となった。
ただ、[next(gen) for i in range(100 - 2)]
とあるように、好きなインデックスを直接持ってくることはできず、そのインデックスまでnext
を繰り返さないといけない。
しかし、かなり重い処理をしたい、かつ、順番にデータを使用する、という場面であればgenerator
を使用するとかなり早く処理を行うことが出来ると思われる。
zip
を複数回使用するためには
初めの問題へと戻り、zip
の中身が消える問題は以下が原因であった。
- python3からは
zip
はイテレータ扱い - イテレータは一回使用するとその要素は削除される
では、zipを複数回使用したい場合はどうすれば良いのか。
毎回定義する
まずはシンプルに毎回定義する。z
が消えるのであれば、次に使うまでに定義すればよい。
a = [1, 2, 3] b = (10, 2, 30) for i in range(3): z = zip(a, b) print('loop{}'.format(i + 1)) for j in z: print(j) # loop1 # (1, 10) # (2, 2) # (3, 30) # loop2 # (1, 10) # (2, 2) # (3, 30) # loop3 # (1, 10) # (2, 2) # (3, 30)
イテラブルに変換
次はもはやイテレータを使わない方法。イテレータを使用しなければ今回の問題は起きない。
z = list(zip(a, b)) print(z) for i in range(3): print('loop{}'.format(i + 1)) for j in z: print(j) # [(1, 10), (2, 2), (3, 30)] # loop1 # (1, 10) # (2, 2) # (3, 30) # loop2 # (1, 10) # (2, 2) # (3, 30) # loop3 # (1, 10) # (2, 2) # (3, 30)
zipまとめ
本記事をまとめると。
- python3の
zip
はイテレータ - イテレータは要素の取り出し場所を保持
- イテレータは要素を取り出した後はその要素を削除
- ジェネレータはイテレータを作るための関数
iter()
でイテレータを作成next()
でイテレータの次の要素を取り出しyield()
でジェネレータの次の要素を取り出し
日々の気づきを自分の中にzip
する
今回は執筆者自身が実際につまづいた、疑問に思った事案について調べて紹介した。厳密には違う部分があるかもしれないが、今回の内容を皮切りにさらに知識を蓄積したい。
このような日々の気づきの積み重ねが自分を作っていると思う。
参考にさせていただいたサイト
関連記事
-
-
【Mac&スクショ名】Macのスクショのファイル名を自動変更
続きを見る
-
-
【PEP8&flake8】pythonにおけるPEP8とflake8
続きを見る