検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>
effective_python

【 Effective Python, 2nd Edition 】入力元のデータサイズが大きい場合は、リスト内包表記 ( list comprehension ) ではなくジェネレータ式 ( generator expression ) の利用を検討しよう! 投稿一覧へ戻る

Published 2020年6月30日21:59 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

リスト内包表記では、入力元シーケンスの各要素に1対1で対応した要素からなる新たなリストインスタンスが作成される場合もあります。
この場合、入力元のデータが非常に大きければメモリ消費もそれに伴って大きくなってしまいます。


例えば、テキストファイルを読み込み、各行に含まれる文字数を取得するリスト内包表記を記述するとしましょう。
最終的には、ファイルの行数文の文字数をセットするためのメモリ領域が必要です。
対象とするファイルが巨大であったり、対象が延々と送られてくるネットワークソケットであったりしたらどうなっちゃうんでしょう?


対象のファイルが小さい場合であれば以下のような実装で問題ないでしょう。

my_file.txt:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nam in varius dolor. Ut quam quam, lobortis quis odio eu, consectetur gravida purus.
Mauris elit purus, tristique ut auctor sed, accumsan vel purus.
Nulla ornare aliquam augue, vitae gravida augue sodales non.
Nullam laoreet enim orci.
Vel interdum justo convallis a. Aenean sit amet commodo libero. Vestibulum ante.
Ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae.



value = [len(x) for x in open('my_file.txt', 'r')]

print(value)
# [57, 85, 64, 61, 26, 81, 71]



巨大なデータを扱う際に生じる問題に対処するために、Python にはジェネレータ式 ( generator expression ) が用意されています。


ジェネレータ式は通常のジェネレータと同様に実行時に全ての出力結果をメモリに保存するわけではなく、ジェネレータイテレータを返すだけです。


it = (len(x) for x in open('my_file.txt', 'r'))

print(it)
# <generator object <genexpr> at 0x0000000002468900>



そして以降の next() を利用した呼出し毎に式を1回だけ実行しその結果を返します。
つまり、入力元のデータがどれほど大きかろうが、式を1回実行しそこから返される値を保持するためだけのメモリしか消費しません。


print(next(it))
# 57

print(next(it))
# 85



ジェネレータ式の特筆すべきもう1つの点は、複数のジェネレータ式をネスト表記できる、ということです。


# 内部のジェネレータイテレータ (it) から取得した値について、その値そのものとその値の平方根からなるタプルを返すジェネレータイテレータ (roots)
roots = ((x, x**0.5) for x in it)

print(next(roots))
# (64, 8.0)

print(next(roots))
# (61, 7.810249675906654)



このイテレータ ( roots ) を1つ進めると、含まれているイテレータ ( it ) も1つ進められる、というように連鎖的に動作が実行されます。
結果的に、通常の内包表記と同様 for 文 -> if 文 -> 値の代入文 の順番で式の処理が進み、値が1つ返された時点で全体が停止、メモリもこの分だけが消費されるだけ、ということになります。
そして、Python におけるこのような連鎖的ジェネレータ式 ( chaining generators ) の実行速度は非常に速いんです。


もし、巨大なストリームデータを安全に、効率的に処理する方法が必要な場面に出くわした場合は、是非ジェネレータ式の活用を考えてみてください。


ただし、ジェネレータ式から返ってくるのはあくまでもイテレータである、ということを忘れてはいけません。


ある関数への引数としてジェネレータ式からの返却値 (イテレータ) を渡し、もしその関数内で total = sum(iterator) と for x in iterator: というイテレータを参照する2つの処理を行っていた場合、2回目にイテレータを参照する for 文では値を受け取ることはできません。


それは、ジェネレータからのイテレータが stateful (どこまで実行したのかを記憶している) であり、データの最後まで実行され StopIteration 例外を投げると、再度先頭から実行を繰り返すことができないためです。


このことについては 前回の記事 で詳しく取り上げていますので、興味のある方は読んでみてください。


まとめ:

1: リスト内包表記で大きな入力データを扱う場合はメモリ消費への注意を怠ってはいけません。

2: ジェネレータ式はイテレータを返し、1回の実行毎に1つの値を返す分のメモリしか消費しません。

3: 1つのジェネレータ式をもう1つほかのジェネレータ式の for 文の入力元としてネストさせて利用することができます。

4: 3 のような連鎖的ジェネレータ書式の処理速度は非常に速く、またメモリも効率的に利用できます。

この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【 Effective Python, 2nd Edition 】assignment expression (walrus operator, :=) を利用して内包表記におけるサブ書式の重複を回避しよう!
【 Effective Python, 2nd Edition 】ジェネレータ ( generator ) に値を注入したいなら、yield from 式と send() 関数の併用よりも、注入する値を提供するイテレータ ( iterator ) を渡しましょう、の巻
【 Effective Python, 2nd Edition 】プログラムを並列処理 ( concurrency ) パターンへ移行するタイミングとツールを考えるシリーズ 第 5 回 - 並列処理 ( concurrency ) のためにスレッド ( thread ) を利用する場合は concurrent.futures モジュールの ThreadPoolExecutor の導入を検討しましょう、の巻
【Python 雑談・雑学】 list comprehension (リスト内包表記) は節度を持って使ってください、という話
【 Effective Python, 2nd Edition 】throw() メソッドを利用したジェネレータ ( generator ) 内部での状態遷移はなるだけ避けましょう。ネストが深くなってコードの読解性が落ちちゃいますよ!
【Python 雑談・雑学 + coding challenge】iterator protocol の実装 --- __iter__ 特殊関数は何を返すべき? イテレータオブジェクト ( iterator object ) なら何でも、そう、generator expression でもOKです!
【 Effective Python, 2nd Edition 】引数として受け取った値を関数内で複数回「消費」する場合には要注意! イテレータ ( iterator ) とコンテナ ( container ) の違いをちゃんと認識しよう!