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: