【 Effective Python, 2nd Edition 】リストを返す関数を作るなら、ジェネレータを返しちゃダメか1回立ち止まって考えよう、の巻 投稿一覧へ戻る
Published 2020年6月27日19:16 by mootaro23
SUPPORT UKRAINE
- Your indifference to the act of cruelty can thrive rogue nations like Russia -
結果としてシーケンスを返す関数を作成する場合、最も一般的な選択肢は「リストを返す」ということでしょう。
次の例は、受け取った文字列について、文字列中の各単語の先頭インデックス番号と該当する単語のタプルからなるリストを返すものです。
この実装で気になるのは、見つかった単語をセットし結果的に呼び出し元に返す result リストがイヤに目に付くことです。
定義で1回 (result = [])、単語の追加で2回 (result.append)、返却で1回 (return result) ですね、目立ってます。
そしてより重視すべきは、メモリの消費量の問題です。
この実装方法だと、返却値は一旦リストに全てセットされます。つまり、そのリストを保持しておくだけのメモリが消費される、ということです。
もし入力されるデータが膨大なものだったら、おおっ恐ろしい、プログラムがクラッシュすることだってあるかもしれません!
これら2つの問題を解決するより良い実装方法はジェネレータ (generator) を利用することです。
ご存知のようにジェネレータは yield 書式を含む関数のことで、呼び出されてもすぐに実行されるわけではありません。
その代わりに直ちにイテレータを返します。
このイテレータは next() で呼び出される度に yield 書式の結果を返し、次の yield 書式の直前まで実行し、そこでまた次の呼び出しを待ちます。
さて、上の関数と同じ結果を返すジェネレータ関数を定義してみましょう。
どうでしょうか?リスト result の記述が全てなくなってスッキリしたのではないでしょうか?
また、その都度返す値を処理するメモリを消費するだけですから、リストを利用している場合と違って枕を高くして眠れます。
イテレータからは next() を利用して結果を1つずつ取得することも、この例のように list() に渡してリストに変換することも簡単にできます。
入力されるデータが大きい可能性を完全に否定できない場合はジェネレータの利用を考えてみましょう。
次の例は、受け取った文字列について、文字列中の各単語の先頭インデックス番号と該当する単語のタプルからなるリストを返すものです。
sentence = "Nana is cute and very popular among my friends."
def index_words(text):
result = []
prev_index = 0
for index, letter in enumerate(text):
if letter == ' ':
result.append((prev_index, text[prev_index:index]))
prev_index = index + 1
result.append((prev_index, text[prev_index:len(text)]))
return result
result_list = index_words(sentence)
print(result_list)
# [(0, 'Nana'), (5, 'is'), (8, 'cute'), (13, 'and'), (17, 'very'), (22, 'popular'), (30, 'among'), (36, 'my'), (39, 'friends.')]
def index_words(text):
result = []
prev_index = 0
for index, letter in enumerate(text):
if letter == ' ':
result.append((prev_index, text[prev_index:index]))
prev_index = index + 1
result.append((prev_index, text[prev_index:len(text)]))
return result
result_list = index_words(sentence)
print(result_list)
# [(0, 'Nana'), (5, 'is'), (8, 'cute'), (13, 'and'), (17, 'very'), (22, 'popular'), (30, 'among'), (36, 'my'), (39, 'friends.')]
この実装で気になるのは、見つかった単語をセットし結果的に呼び出し元に返す result リストがイヤに目に付くことです。
定義で1回 (result = [])、単語の追加で2回 (result.append)、返却で1回 (return result) ですね、目立ってます。
そしてより重視すべきは、メモリの消費量の問題です。
この実装方法だと、返却値は一旦リストに全てセットされます。つまり、そのリストを保持しておくだけのメモリが消費される、ということです。
もし入力されるデータが膨大なものだったら、おおっ恐ろしい、プログラムがクラッシュすることだってあるかもしれません!
これら2つの問題を解決するより良い実装方法はジェネレータ (generator) を利用することです。
ご存知のようにジェネレータは yield 書式を含む関数のことで、呼び出されてもすぐに実行されるわけではありません。
その代わりに直ちにイテレータを返します。
このイテレータは next() で呼び出される度に yield 書式の結果を返し、次の yield 書式の直前まで実行し、そこでまた次の呼び出しを待ちます。
さて、上の関数と同じ結果を返すジェネレータ関数を定義してみましょう。
def index_words_gen(text):
prev_index = 0
for index, letter in enumerate(text):
if letter == ' ':
yield prev_index, text[prev_index:index]
prev_index = index + 1
yield prev_index, text[prev_index:len(text)]
it = index_words_gen(sentence)
result_list = list(it)
print(result_list)
# [(0, 'Nana'), (5, 'is'), (8, 'cute'), (13, 'and'), (17, 'very'), (22, 'popular'), (30, 'among'), (36, 'my'), (39, 'friends.')]
prev_index = 0
for index, letter in enumerate(text):
if letter == ' ':
yield prev_index, text[prev_index:index]
prev_index = index + 1
yield prev_index, text[prev_index:len(text)]
it = index_words_gen(sentence)
result_list = list(it)
print(result_list)
# [(0, 'Nana'), (5, 'is'), (8, 'cute'), (13, 'and'), (17, 'very'), (22, 'popular'), (30, 'among'), (36, 'my'), (39, 'friends.')]
どうでしょうか?リスト result の記述が全てなくなってスッキリしたのではないでしょうか?
また、その都度返す値を処理するメモリを消費するだけですから、リストを利用している場合と違って枕を高くして眠れます。
イテレータからは next() を利用して結果を1つずつ取得することも、この例のように list() に渡してリストに変換することも簡単にできます。
入力されるデータが大きい可能性を完全に否定できない場合はジェネレータの利用を考えてみましょう。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -