SE_BOKUのまとめノート的ブログ

SE_BOKUが知ってること・勉強したこと・考えたことetc

Python3の「内包表記」・「ジェネレータ式」・「ヘルパー関数」の使い方サンプル/Python文法

f:id:arakan_no_boku:20190427205325j:plain

目次

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]

という感じで、内包表記の構文と左と右が入れ替わるので注意が必要です。

forループとどのくらい速度が違うかを計測してみます。 

上記をそれぞれ関数にして、1回あたり10000回のループを1000回繰り返した結果の時間を出してます。

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 = {'高橋君': 'A001', '松本君': 'A002', '石田君': 'A003'}
rank_name = {rank: name for name, rank in name_rank.items()}
print(rank_name)

このプリントの結果はこうなります。 

 {'A001': '高橋君', 'A002': '松本君', 'A003': '石田君'}

よく見れば

 name, rank in name_rank.items()

のように、辞書のキーを「name」、値を「rank」として取り出して、それを

rank: name

としてセットしているだけなのがわかると思います。

3:集合(Set)の内包表記

Setの内包表記は、こんな感じです。

{格納する要素 for 配列内要素 in 配列}

例をやってみます。使うデータは以下です。

name_rank = {'高橋君': 'A001', '松本君': 'A002', '石田君': 'A003'}

この辞書のキーだけをとりだして「名前(name)」の集合(Set)を作ります。

name_rank = {'高橋君': 'A001', '松本君': 'A002', '石田君': 'A003'}
name_set = {name for name in name_rank.keys()}
print(name_set)

このプリントの結果はこんな感じ 

{'石田君', '高橋君', '松本君'}

 キーの名前だけの集合(Set)になってます。

内包表記の問題点と対処法 

内包表記は、慣れると式が少なければ意外に読みやすいですし、処理速度も速いです。

ただ、2つ問題があります。

  • 入力するデータ量が大量だと、大きなメモリを消費してしまう。
  • 内包表記の式の数が増えると、非常に読みづらくなる

です。

これらに対する対策法です。 

メモリ消費問題を回避する:ジェネレータ式 

例えば、リスト内包表記でこんな使い方をした場合

value_list = [len(x) for x in open('big_text_file.txt')]

openするファイルが巨大であっても一気にメモリに読み込もうとします。

結果、最悪プログラムがクラッシュするリスクがあります。

これを回避するには「ジェネレータ式」を使います。

書き方はこうです。

value_list = (len(x) for x in open('big_text_file.txt'))

リスト内包表記構文を()で囲むだけです。

この書き方をすると、valueにデータは展開されず、イテレータとして評価されます。

それを取り出す方法は2通り。

  • 次の値ひとつを取り出す「next(value_list)」
  • forループの中でイテレータとして処理する

あります。

こうすると、元データがどんなに大きくても問題なくなります。

ただ、このイテレータは1回しかforループで使えない制約があります。

繰り返して使うと、毎回、同じ結果が返ってくることが保証されません。

この点だけは注意が必要です。

式の数が増えて読みづらくなる問題を回避する:ヘルパー関数 

内包表記はネストが増えたり、条件分岐がはいると非常に読みづらくなります。

例えば。

こんな処理を考えます。

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通り。

  • 次の値ひとつを取り出す「next(value_list)」
  • forループの中でイテレータとして処理する

です。

さきほどの例を「ジェネレータ」化した例はこんな感じです。

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ループの中でイテレータとして処理)を選んで、ループを回して受取側で一次元リストを組み立てている感じになります。

若干、関数の利用者側が面倒ですが「メモリ不足な環境なのに、データ量が多くて、でかいリストができてしまう恐れがある」場合などの対策にはなりえます。

ではでは。