目次
pythonの内包表記構文・ジェネレータ・ヘルパー関数
内包表記は、forループを簡潔に書けて、かつ、圧倒的に実行速度が速い優れモノです。
配列の初期化などによく使います。
特に二次元配列などの初期化にはかかせません。
バリエーションとして。
- listの内包表記
- 辞書の内包表記
- 集合(Set)の内包表記
があります。
今回は、その内包表記と、その問題点をカバーする方法になっている「ジェネレータ」と「ヘルパー関数」について整理します。
1:listの内包表記
一番シンプルなリストの例で書くと。
こういうforループと同じものが。
s_list = [] for i in range(num): s_list.append(i)
内包表記 にするとこんな感じで書けます。
s_list = [i for i in range(num)] に
二次元のリストだとこうなります。
s_list = [[i for x in range(num)] for i in range(num)]
内側のループが1次元目、外側のループが二次元目に対応します。
結果生成される二次元リストに添え字をはめてみると
s_list[i][x]
という感じで、内包表記の構文と左と右が入れ替わるので、一瞬勘違いしやすいです。
ちょっと、ややこしく見えますが、こう書くだけで速度がぐっと速くなります。
試しに、どのくらい速度が違うかを「timeit」を使って計測してみます。
上記をそれぞれ関数にして、1回あたり10000回のループを1000回繰り返した結果の時間を出してます。
1回とかにすると、時間が短すぎるので。
import timeit def sample_loop(num): L = [] for i in range(num): L.append(i) return L def sample_comprehension(num): return [i for i in range(num)] repeat_count = 10000 loop_count = 1000 result = timeit.timeit( 'sample_loop(repeat_count)', globals=globals(), number=loop_count) print(result) result = timeit.timeit( 'sample_comprehension(repeat_count)', globals=globals(), number=loop_count) print(result)
自分のPCで実行してみた結果はこちらです。
通常のループ:785ミリ秒
内包処理使用:379ミリ秒
だいたい、倍くらい違いますね。
2:辞書の内包表記
辞書の内包表記の構文の意味合いはこんな感じです。
{格納する要素のkey:格納する要素のvalue for 配列内要素 in 配列}
ただ、日本語で理解しようとしても、たぶん、よくわからないので例で書きます。
例えば。
name_rank = {'高橋君': 'A001', '松本君': 'A002', '石田君': 'A003'}
このように「名前(name)」がキーで、「格付(rank)」が値(Value)の辞書があった時に、キーと値を逆転させて、「格付(rank)」がキーで、「名前(name)」が値(value)のの辞書にする場合なら以下のようになります。
name_rank = {'高橋君': 'A001', '松本君': 'A002', '石田君': 'A003'} rank_name = {rank: name for name, rank in name_rank.items()} print(rank_name)
このプリントの結果はこうです。
{'A001': '高橋君', 'A002': '松本君', 'A003': '石田君'}
意図の通りに入れ替わってます。
3:集合(Set)の内包表記
Setの内包表記は、辞書のValueがないようなもんなので、こんな感じです。
{格納する要素 for 配列内要素 in 配列}
これも例で見た方がわかりやすいので、上記辞書で使ったものを使ってやってみます。
name_rank = {'高橋君': 'A001', '松本君': 'A002', '石田君': 'A003'}
このように「名前(name)」がキーで、「格付(rank)」が値(Value)の辞書があった時に、キーの「名前(name)」の集合(Set)を作る場合にはこうなります。
name_rank = {'高橋君': 'A001', '松本君': 'A002', '石田君': 'A003'} name_set = {name for name in name_rank.keys()} print(name_set)
このプリントの結果はこんな感じ
{'石田君', '高橋君', '松本君'}
キーの名前だけの集合(Set)になってます。
ただ、辞書の時の並び順にはならなないという所は注意が必要ですけどね。
内包表記の抱える2つの問題点
内包表記は、慣れると式が少なければ意外に読みやすいですし、処理速度も速いです。
積極的に利用する価値はあります。
ただ。
2つ問題があります。
- 入力するデータ量が大量だと、大きなメモリを消費してしまう。
- 内包表記の式の数が増えると、非常に読みづらくなる
です。
これらに対する対策法です。
メモリ消費問題を回避する:ジェネレータ式
例えば、リスト内包表記のこんな使い方が該当します。
value_list = [len(x) for x in open('big_text_file.txt')]
巨大なデータを一気にメモリに読み込もうとするので、ファイルがでかいと、最悪プログラムがクラッシュします。
これを回避するには「ジェネレータ式」を使います。
書き方はこうです。
value_list = (len(x) for x in open('big_text_file.txt'))
リスト内包表記構文を()で囲むだけです。
この書き方をすると、valueにデータは展開されず、イテレータとして評価されます。
それを取り出す方法は2通り。
あります。
こうすると、元データがどんなに大きくても問題なくなります。
ただ、1点だけ注意が必要な部分があります。
それは、ジェネレータ式が返すイテレータは繰り返して使った時に、毎回、同じ結果が返ってくることが保証されないことです。
ジェネレータ式の結果は、1回限りの利用を前提に考える必要があります。
式の数が増えて読みづらくなる問題を回避する:ヘルパー関数
内包表記はネストが増えたり、条件分岐がはいると非常に読みづらくなります。
例えば。
こんな処理を考えます。
tmp_lists = [ [[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]], ] flat_list1 = [x for sub1 in tmp_lists for sub2 in sub1 for x in sub2] print(flat_list1)
tmp_lists という3次元リストを、1次元リストにフラット化する処理です。
上記の内包表記によって「[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]」という1次元リストに変換されるのですが、こうなるとパッと見て理解するのは難しいです。
ソースは読みやすく見通しが良くしておかないと、後で困るので、これは、以下のように普通にforをネストさせて、その処理を関数化した方がいいとされてます。
def to_flat_list(tmp_lists_arg): flat_list = [] for sub1 in tmp_lists_arg: for sub2 in sub1: flat_list.extend(sub2) return flat_list
これの利用イメージです。
tmp_lists = [ [[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]], ] flat_list2 = to_flat_list(tmp_lists) print(flat_list2)
こういうやり方を「ヘルパー関数」といいます。
これも結果は「[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]」という1次元リストに変換されます。
ヘルパ関数で扱うデータが巨大な場合に対応する:ジェネレータ式
ヘルパ関数の場合でもデータが巨大な場合にメモリを食いつぶすリスクはあります。
こんな時も「ジェネレータ式」で対応します。
ヘルパ関数の場合のジェネレータは「yield」を使います。
ループの中で返したい値の生成時に「yield」をつけると、上で説明したジェネレータ式と同様にイテレータを返すヘルパ関数になります。
替えられたイテレータ\から値を取り出す方法は2通り。
です。
さきほどの例を「ジェネレータ」化した例はこんな感じです。
def to_flat_list_yield(tmp_lists_arg): flat_list = [] for sub1 in tmp_lists_arg: for sub2 in sub1: yield sub2
この利用例です。
tmp_lists = [ [[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]], ] tmp_gen = to_flat_list_yield(tmp_lists) flat_list3 = [] for x in tmp_gen: flat_list3.extend(x) print(flat_list3)
returnではなく、yieldで返します。
returnは処理済の結果を返しますが、yieldはイテレータを返します。
なので、上記のtmp_genの場合だと
- 次の値ひとつを取り出す「next(tmp_gen)」
- forループの中でイテレータとして処理する
の2通りのやり方の中から後者(forループの中でイテレータとして処理)を選んで、ループを回して受取側で一次元リストを組み立てている感じになります。
若干、関数の利用者側が面倒ですが「メモリ不足な環境なのに、データ量が多くて、でかいリストができてしまう恐れがある」場合などの対策にはなりえます。
ではでは。