当サイトは、アフィリエイト広告を利用しています
pPythonでリストから特定の値を取得する際、
のどちらを使うか迷う場面がある。
実際のソースとしては構文上の違いは小さい。
# リスト内包表記[式 for x in iterable if 条件]# ジェネレータ式(式 for x in iterable if 条件)
が、構造が違うためメモリの使い方が違い、それゆえに
設計上の意味あいが大き変わる。
当記事では
を整理する
ジェネレータ式はpythonで効率的にデータを処理する方法の一つで
遅延評価を利用して要素を一つずつ生成する方法のこと。
具体的にはリストなどの反復可能オブジェクト(iterable)から
ジェネレータオブジェクトを生成する
ジェネレータ式の目的としては
になる。
取り出した値は必要な時に遅延評価をして使う
ジェネレータオブジェクトはジェネレータ式を実行した結果生成される
特殊なオブジェクトのこと。
ジェネレータオブジェクトは遅延評価の仕組みを採用しており
値が必要になるまで計算はしない。
そのため、データを使用する場合はジェネレータオブジェクトから
値を取り出す必要がある。
# 反復可能オブジェクトusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# terget_userはジェネレータオブジェクトterget_user = (user for user in users if user["user_id"] == "1")
terget_userhはジェネレータオブジェクトであるため
使うには値を取り出す必要がある
※このままでは何もできない
ジェネレータオブジェクトの仕組みである遅延評価は
作成時に即座に値を計算するのではなく、必要な時に評価して使う仕組みのこと。
評価方法としては主に
がよく使われる
※詳しくは後述する
ジェネレータ式は上記の通り、遅延評価のジェネレータオブジェクトを生成するので
必要な時まで計算を行わない。つまり、評価するまではメモリを消費しない。
そのため
の利点がある。
ジェネレータ式の具体的な使い方をまとめる
ジェネレータ式の構文は
(式 for 変数 in iterable if 条件)
のようになる
※()以外はリスト内包表記と同じ。
# ディクショナリリストusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# ジェネレータ式でジェネレータオブジェクト生成user_names = (user["name"] for user in users)
ディクショナリリストから「name」だけのジェネレータオブジェクトを生成
この段階ではuser_namesはまだなにも計算していない。
# ディクショナリリストusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# ジェネレータ式でジェネレータオブジェクト生成user_names = (user["name"] for user in users)# forで遅延評価して値を取り出すfor name in user_names:print(name) # ['Tujimura', 'mori', 'shimada', 'kyogoku']
for文を使って値を評価してprintで出力している。
next()を使う場合は下記のようになる。
# ディクショナリリストusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# ジェネレータ式でジェネレータオブジェクト生成user_names = (user["name"] for user in users)# nextで遅延評価して値を取り出すprint(next(user_names)) # Tujimuraprint(next(user_names)) # moriprint(next(user_names)) # shimadaprint(next(user_names)) # kyogokuprint(next(user_names)) # StopIteration
nextを使って順次、ジェネレータオブジェクトを遅延評価して取り出す。
値がなくなるとStopIterationとなる。
ジェネレータオブジェクトを注意点として
というのがあるので注意。
# nextで遅延評価して値を取り出すprint(next(user_names)) # Tujimuraprint(next(user_names)) # moriprint(next(user_names)) # shimadaprint(next(user_names)) # kyogokuprint(next(user_names)) # StopIteration
でいうと、next()で前の値に戻ることはできないということ。
また一度使い切ったジェネレータは再利用できない。
使いどころとしてはリストなどの反復可能なオブジェクトなオブジェクトから
指定した条件のデータを取得する等のリスト内包表記と同じような使い方ができる
使い方としてリスト内包表記と同じなので、処理対象のデータが大量データであったり
またメモリを効率良く使う必要がある等の対象データで判断する。
代表的な使用パターン
を実装して確認する
ジェネレータ式を使ってリストの中からデータを1件抽出する場合
となり、これがジェネレータ式の代表的な使いどころ。
1件だけ取得なので、next()を使って値を取り出す
# 辞書リストusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "1", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# ジェネレータ式で抽出(2件とれる)terget_user = (user for user in users if user["user_id"] == "1")# 遅延評価で最初の1件取得terget_user_val = next(terget_user)print(terget_user_val)# 実行結果# {'user_id': '1', 'name': 'Tujimura', 'age': 11}
リストの値を1件ずつ取り出し、逐次処理する場合は
のでジェネレータ式を使うほうがメモリ効率がよくなる
逐次処理なので、for文を使って値を取り出す
# 辞書リストusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# ジェネレータ式で1件を抽出terget_user = (user for user in users if user["user_id"] == "1")# 遅延評価(for)for user in terget_user:print(user)# 実行結果# {'user_id': '1', 'name': 'Tujimura', 'age': 11}
リストに対してパイプライン的に処理する場合は
のでジェネレータ式を使うほうがメモリ効率がよくなる
# 辞書リストusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# ① user_id が "1" のユーザーを抽出filtered = (user for user in users if user["user_id"] == "1")# ② name だけに変換names = (user["name"] for user in filtered)# ③ 文字列長が5以上のものだけ通す(例)validated = (name for name in names if len(name) >= 5)# ④ 最終出力for name in validated:print(name)# 実行結果# Tujimura
この設計のポイントとしては
つまりこれは
構造になっている
関数化してしまう方法もある
def filter_user(users):return (user for user in users if user["user_id"] == "1")def extract_name(users):return (user["name"] for user in users)def validate_name(names):return (name for name in names if len(name) >= 5)pipeline = validate_name(extract_name(filter_user(users)))for name in pipeline:print(name)
## ジェネレータ式をつかうべきではないケース
上記ではジェネレータ式を使った有効利用パターンを示してたが
逆に使うべきでないアンチパターンもまとめておく
代表的なアンチパターン
を実装して確認する
ジェネレータ式でパイプラインを作っても、最終的にlist()で全件保持するなら
最初からリスト内包表記で良いケースが多い
# 辞書リストusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# パイプライン(遅延評価)filtered = (user for user in users if user["age"] >= 20)names = (user["name"] for user in filtered)validated = (name for name in names if len(name) >= 5)# ここで全件保持してしまう(遅延評価のメリットが消える)result = list(validated)print(result)# 実行結果: ['shimada', 'kyogoku'] など
これを内包表記なら1発で書ける
result = [u["name"] for u in users if u["age"] >= 20 and len(u["name"]) >= 5]print(result)# 実行結果: ['shimada', 'kyogoku'] など
ジェネレータは 一度消費すると戻れない。
複数回使う前提なら、最初からリストで持つほうが自然である。
users = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]validated = (user["name"]for user in usersif user["age"] >= 20 and len(user["name"]) >= 5)# 1回目の走査(OK)print(list(validated)) # ['shimada', 'kyogoku']# 2回目の走査(空になる)print(list(validated)) # []
複数回使うならlist内包表記で書くほうが自然である。
validated_list = [user["name"]for user in usersif user["age"] >= 20 and len(user["name"]) >= 5]print(validated_list) # 何度でも使える
ジェネレータは
用途として「先頭から順に流す」以外には向かない。
users = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]validated = (user["name"]for user in usersif user["age"] >= 20)# 長さが欲しい(できない)# print(len(validated)) # TypeError# 3番目が欲しい(できない)# print(validated[2]) # TypeError
これがしたいなら、listで保持すべき
validated_list = [user["name"] for user in users if user["age"] >= 20]print(len(validated_list)) # 3print(validated_list[1]) # 'shimada' など
ジェネレータを使うと逆に不利になるパターンもまとめておく。
いわゆるジェネレータ式のデメリット。
ジェネレータは「値を流す」構造なので、
途中で print(list(gen)) のように中身確認をすると その時点で消費される。
# 辞書リストusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]validated = (user["name"]for user in usersif user["age"] >= 20)# デバッグのつもりで中身を確認(ここで全消費される)print(list(validated)) # ['mori', 'shimada', 'kyogoku']# その後に処理しようとしても、もう空for name in validated:print(name) # 何も出ない
対策としては
list() で受ける(=遅延評価の利点は捨てる)ジェネレータ式を使っていても、途中で list() を挟んだ瞬間に
全件メモリ展開が確定する。
users = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# パイプライン(遅延評価)gen = (user for user in users if user["age"] >= 20)# ここで「確認のため」「使い回しのため」と list 化してしまうtmp = list(gen) # ← この時点で全件保持(遅延評価のメリットが消える)# 以降は結局 list を処理しているだけnames = (user["name"] for user in tmp)print(list(names)) # ['mori', 'shimada', 'kyogoku']
多段ジェネレータを「全部1行でつなぐ」と、
どこで何をしているか分かりにくくなり、例外も追いづらい。
users = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70},{"user_id": "5", "name": None, "age": 30}, # 例外の原因を混ぜる]# どこで壊れたのか分かりにくいpipeline = (name.strip().lower()for name in (u["name"] for u in users if u["age"] >= 20)if len(name) >= 5)for x in pipeline:print(x)# 実行時に AttributeError: 'NoneType' object has no attribute 'strip' 等
この例では name が None があるとに壊れるが、
パイプラインが潰れているため原因箇所が見えにくい。
def select_users(users):return (u for u in users if u["age"] >= 20)def extract_name(users):return (u["name"] for u in users)def normalize(names):# ここが落ちうる(Noneなど)for name in names:yield name.strip().lower()def filter_len(names):return (name for name in names if len(name) >= 5)u1 = select_users(users)n1 = extract_name(u1)n2 = normalize(n1)n3 = filter_len(n2)for x in n3:print(x)
list() を挟んで調査もできるジェネレータ式かリスト内包表記かどちらを使うべきか
判断基準は一言いうと
であるが、細かいチェックリストにすると下記のようになる
len() が必要x[0])が必要迷ったらまずリストで書き、必要になった時にジェネレータへ置き換えるという選択もあり。
ジェネレータの利点を生かした有効活用のについては下記記事でも紹介している