Python3 のチュートリアルを流してみたので、その際に面白いと感じたところのメモです。
参考 Python チュートリアル
動作環境
$ python --version Python 3.7.3
はじめに
python の for 文は、イテレータから値を取り出して繰り返すだけ。
JavaやC言語のように、条件式に基づいて繰り返し判定をすることはない。
Python 言語リファレンス 8.3. for 文
使い方
リストの要素に対して繰り返す。
>>> for x in ['tic', 'tac', 'toe']: ... print(x) ... tic tac toe ## reversed で逆順にできる >>> for x in reversed(['tic', 'tac', 'toe']): ... print(x) ... toe tac tic ## sorted で整列できる >>> for x in sorted(['tic', 'tac', 'toe']): ... print(x) ... tac tic toe ## enumerate でインデックスを取得できる >>> for i, v in enumerate(['tic', 'tac', 'toe'], 1): ... print(i, v) ... 1 tic 2 tac 3 toe
指定回数だけ繰り返すときは range
を使う。
>>> for x in range(5,10): ... print(x) ... 5 6 7 8 9 ## 何個ずつ繰り上げるか指定する >>> for x in range(5, 10, 2): ... print(x) ... 5 7 9
文字列に対して繰り返すこともできる。
>>> for x in "hello": ... print(x) ... h e l l o
辞書型に対して繰り返すこともできる。
>>> house_words = { ... 'Baratheon': 'Ours is the Fury', ... 'Greyjoy': 'We Do Not Sow', ... 'Lannister': 'A Lannister always pays his debts', ... 'Stark': 'Winter is Coming', ... } # キーだけ取得する >>> for x in house_words.keys(): ... print(x) ... Baratheon Greyjoy Lannister Stark ## 値だけ取得する >>> for x in house_words.values(): ... print(x) ... Ours is the Fury We Do Not Sow A Lannister always pays his debts Winter is Coming ## キーと値を取得する >>> for k, v in house_words.items(): ... print(k, v) ... Baratheon Ours is the Fury Greyjoy We Do Not Sow Lannister A Lannister always pays his debts Stark Winter is Coming
else を使うことで for の終わりに処理できる。
>>> for x in range(2): ... print(x) ... else: ... print("done") ... 0 1 done # break で抜けると else は処理されない >>> for x in range(2): ... break ... else: ... print("done") ...
for文 の仕組み
- for文に指定されたオブジェクトの
__iter__()
メソッドを呼び出し、イテレータを取得する - イテレータの
__next__()
を呼び出す - 戻り値を変数に代入し、forブロックの処理する
- StopIteration 例外が返ってきたら、繰り返しを中断する
参考 Python ドキュメント 用語集 イテレータ 参考 Python ドキュメント Python 標準ライブラリ イテレータ型
実際に __iter__()
や__next__()
を呼び出してみると、要素が順番に取得できることがわかる。
>>> it = [1,2].__iter__() >>> it.__next__() 1 >>> it.__next__() 2 >>> it.__next__() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> it.__next__() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
注意点
__iter__()
や__next__()
の代わりに iter
や next
ビルドイン関数が用意されている。これらの関数は引数オブジェクトがイテラブルか(イテレータを返せる)どうかのチェックを行ってくれたりするので、こちらをを利用すること。
参考 Python 標準ライブラリ 組み込み関数 iter
参考 Python 標準ライブラリ 組み込み関数 next
イテレータはいちど StopIteration に達すると、その後は常に StopIteration を返す。ということは、同じシーケンスに対してforを2回以上は呼び出せないの?と思ったが、毎度新しいイテレータを返すため心配はいらない。
>>> x = [1, 2] >>> x.__iter__() <list_iterator object at 0x7f17726f9c18> >>> x.__iter__() <list_iterator object at 0x7f17726f9b38>
for k, v in ...
はどう実現しているの?
イテレータから値を取り出して変数に代入していく、という仕組みは同じ。
複数の変数に代入する操作は、タプルを使うことで実現している。
タプルは以下のように、要素数をそのまま変数に格納できる型。
Python チュートリアル 5.3. タプルとシーケンス
>>> x, y, z = ('apple', 'banana', 'cherry') >>> print(x) apple >>> print(y) banana >>> print(z) cherry
イテレータでタプルを返すことで、変数が2以上の場合に対応している。
>>> it = { 'alice': 'apple', 'bob': 'banana'}.items().__iter__() >>> type(it) <class 'dict_itemiterator'> >>> it.__next__() ('alice', 'apple') >>> type(it.__next__()) <class 'tuple'>
注意点その1
変数スコープが for の外側と同じ。
>>> del x >>> for x in range(5): ... print(x) ... 0 1 2 3 4 >>> print(x) 4
これは、そもそもpythonにはブロックスコープという考え方がないため。forに限らず、ifやelseにもブロックスコープがない点は、個人的には面食らった。
参考 TauStation Python3 ? 変数のスコープ
注意点その2
リストの要素が途中で変更された場合、繰り返し項目に反映される。 下手に元のリストに変更を加えると、無限ループを起こす可能性がある。
>>> import time >>> l = [1, 2, 3] >>> for x in l: ... print(x) ... time.sleep(1) ... l.insert(1, -99) ... 1 -99 -99 -99 ^CTraceback (most recent call last): File "<stdin>", line 3, in <module> KeyboardInterrupt
初回のリストで固定したければ、コピーしたものを渡す。
>>> import time >>> l = [1, 2, 3] >>> for x in l[:]: ... print(x) ... time.sleep(1) ... l.insert(1, -99) ... 1 2 3
ジェネレータ
イテレータ( __iter__()
, __next__()
を実装したもの)をお手軽に作ることができる仕組み。
参考 Python ドキュメント 用語集 ジェネレータ
参考 Python 言語リファレンス yield式
以下のようにジェネレータを作成する。
>>> def gen(): ... for item in [1, 2, 3]: ... print("generated ", item) ... yield item # 1, 2, 3 を順番に返す
ジェネレータを実行することで、イテレータ(厳密にはジェネレータイテレータ)が取得できる。
>>> import collections >>> isinstance(gen(), collections.Iterable) True
宣言を確認すると、イテレータに必要な__iter__()
と__next__()
が勝手に実装されていることがわかる。
>>> help(gen()) Help on generator object: gen = class generator(object) | Methods defined here: | | __del__(...) | | __getattribute__(self, name, /) | Return getattr(self, name). | | __iter__(self, /) | Implement iter(self). | | __next__(self, /) | Implement next(self). | | __repr__(self, /) | Return repr(self). | | close(...) | close() -> raise GeneratorExit inside generator. | | send(...) | send(arg) -> send 'arg' into generator, | return next yielded value or raise StopIteration. | | throw(...) | throw(typ[,val[,tb]]) -> raise exception in generator, | return next yielded value or raise StopIteration. |
イテレータは、__next__()
が呼ばれると yield の値を返す。
>>> def gen(): ... for item in [1, 2, 3]: ... print("generated ", item) ... yield item ... >>> for x in gen(): ... print(x) ... # 要素が必要になったタイミングでジェネレータが動く(=遅延評価される) generated 1 1 generated 1 2 generated 1 3
要素の生成は遅延評価されるため、メモリ使用量の節約に役立つ。
遅延評価
ジェネレータに限らず、python のかなりの関数は遅延評価されるようになっている。(ように感じた)
例えば map なんかは、遅延評価されていることが分かりやすい。
>>> for x in map(lambda x: print('mapped'), [1,2,3]): ... print(x) ... mapped None mapped None mapped None
このような遅延処理の仕組みのベースになっているのが、イテレータ。
要素が必要になったときにイテレータの__next__()
を呼び出すことで、必要なものを、必要なときに、必要なだけ処理することができる。
参考 Python: range is not an iterator!
参考 Python2からPython3.0での変更点
>>> import collections # rangeの戻り値はイテレータ >>> isinstance(range(1,5), collections.Iterable) True # mapの戻り値もイテレータ >>> mapped = map(lambda x: x**2, [1,2,3]) >>> isinstance(mapped, collections.Iterable) True # filterの戻り値もイテレータ >>> filtered = filter(lambda x: x % 2 == 0, [1, 2, 3]) >>> isinstance(filtered, collections.Iterable) True
Just In Timeに無駄なく処理する感じがかっこいい:crown: