はじめに
コードに使ったyield
について解説を求められたが、その場で上手く説明できなかったのでここでまとめる。
一応Pythonを念頭に置くが、C#だろうがRubyだろうが(構文は多少変わるが)本質の部分は同じである。
yieldを使うと何が起こるのか?
例えばreturnで実装した場合
>>> def foo():
... return ['foo', 'foo']
...
>>> foo()
['foo', 'foo']
foo()を呼び出すことで配列を得ることができた。この挙動は予想通りだろう。
これをyieldで実装しなおすと、こうなる。
>>> def bar():
... yield 'bar'
... yield 'bar'
...
>>> bar()
<generator object bar at 0x7f519e452840>
returnで実装されたfoo()と異なり、bar()を実行するとgeneratorという謎のオブジェクトが返ってきた。foo()と同じように配列を得るためには、list()という、引数に与えられたものを配列にするための組み込み関数を使用しなければならない。
>>> list(bar())
['bar', 'bar']
これだけだと同じ実行結果を得るための手間が増えただけのように見える。ではyieldで実装すると何が嬉しいか?それを理解するには「遅延評価」について知らなければならない。
先行評価 vs 遅延評価
「評価」とはコードを実行して結果を得ることである。
再度return
とyield
でコードを書き分けて、実行結果の違いを示す。
>>> def foo():
... buffer = []
... for i in range(2):
... print('配列に要素を追加します')
... buffer.append('foo')
... return buffer
...
>>> def main():
... for x in foo():
... print('ループが回りました')
...
>>> main(foo)
配列に要素を追加します
配列に要素を追加します
ループが回りました
ループが回りました
return
で実装すると「foo()全体を評価した後main()に処理が戻る」というおなじみの結果になった。このような評価の仕方を「先行評価」と呼ぶ。
>>> def bar():
... for i in range(2):
... print('配列に要素を追加します')
... yield 'bar'
...
>>> def main():
... for x in bar():
... print('ループが回りました')
...
>>> main(bar)
配列に要素を追加します
ループが回りました
配列に要素を追加します
ループが回りました
一方yield
で実装すると「yield
に到達したら実行を途中でやめてmain()に一度処理を戻す」という動きをする。言い換えれば「main()から要求されるまで続きを評価しない」という実装になっている。このような評価の仕方は「遅延評価」と呼ぶ。
遅延評価が活きるポイント
重い処理がある場合
処理の一部が極端に重いので実行回数をなるべく少なくしたいことがある。
実行回数が事前に分からない場合、遅延評価で実装することによって回数を少なくすることができる。
from time import sleep
from itertools import islice
def very_heavy(s):
# 3の倍数の時だけすごい重くなる
if s % 3 == 0:
sleep(s)
return s
# 先行評価だと、全部評価してしまい非効率
def get_result():
buffer = []
for i in range(10):
buffer.append(very_heavy(i))
return buffer
# 遅延評価なら必要なだけしか評価されない
def get_lazy_result():
for i in range(10):
yield very_heavy(i)
get_result()[:5] # 実行時間は常に18秒
list(islice(get_lazy_result(),5)) # 実行時間は3秒
無限リストの作成
例えば以下のコードは、無限に1を返し続ける。
def infinite_one():
while True:
yield 1
これを応用すると「何かの一覧(事前に件数は分からない)」を簡単に作成することができる。
例えばスクレイピングに応用すると、Googleのようにページングされたサイトであっても簡単に結果の一覧を取得することができる。もちろん遅延評価なので、「上位10件だけ取得したい」という時に無駄なページ遷移は発生しない。
def get_search_results():
while True:
# GoogleのPC検索結果画面はタイトルをh2でマークアップしている
for h2_tag in get_h2_tags():
yield h2_tag
# 次のページがなくなるまで検索結果を取得し続ける
if not move_to_next_page():
break
再帰的な呼び出し
yield
を使用すると再帰呼び出しを簡潔に書くことができる場合がある。
import os
import os.path
def recursive_find_files(path):
for x in os.listdir(path):
join_path = os.path.join(path, x)
if os.path.isdir(join_path):
yield from recursive_find_files(join_path)
else:
yield join_path