Edited at

yieldのメリットを解説する


はじめに

コードに使ったyieldについて解説を求められたが、その場で上手く説明できなかったのでここでまとめる。

一応Pythonを念頭に置くが、C#だろうがRubyだろうが(構文は多少変わるが)本質の部分は同じである。


yieldを使うと何が起こるのか?

例えばreturnで実装した場合このような結果になる。

>>> def foo():

... return ['foo', 'foo']
...
>>> foo()
['foo', 'foo']

これをyieldで実装しなおすと、こうなる。

>>> def bar():

... yield 'bar'
... yield 'bar'
...
>>> bar()
<generator object bar at 0x7f519e452840>
>>> list(bar())
['bar', 'bar']

見ての通り、bar()の実行結果はgeneratorというオブジェクトである。

従ってfoo()と同じ結果を得るためにはさらにlist()関数で計算しなければならない。

ではyieldで実装するメリットは何か?それを理解するには「遅延評価」について知らなければならない。


先行評価 vs 遅延評価

少し補足しておくと、「評価」とはコードを実行して結果を得ることである。

再度returnyieldでコードを書き分けて、実行結果の違いを示す。

>>> def foo():

... buffer = []
... for i in range(2):
... print('foo():printが評価されました')
... buffer.append('foo')
... return buffer
...
>>> def main():
... for x in foo():
... print('main():' + x)
...
>>> main(foo)
foo():printが評価されました
foo():printが評価されました
main():foo
main():foo

returnで実装すると「foo()全体を評価した後main()に処理が戻る」というおなじみの結果になった。このような評価の仕方を「先行評価」と呼ぶ。

>>> def bar():

... for i in range(2):
... print('bar():printが評価されました')
... yield 'bar'
...
>>> def main():
... for x in bar():
... print('main():' + x)
...
>>> main(bar)
bar():printが評価されました
main():bar
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