魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

list 内包表記で遊んでいたら generator に出会った

概要

  • list 内包表記で遊んでいたら generator に出会った。

list 内包表記

list 内包表記は雑に言えば for 文で回した結果を順次 list に格納する際に便利な表記です。 例えば、要素が整数の list に対してその各要素を 2 倍した要素を持つ list を作りたいときはこう書けます。

l = [1, 2, 3, 4]
g = [i * 2 for i in l]

print(g) # [2, 4, 6, 8]

この g = [i * 2 for i in l] の書き方が list 内包表記でした。

無邪気な好奇心

端的に言うと i * 2 for i in l の部分を単体で取り出したらどうなるかが気になったのです。 結果はこうでした。

l = [1, 2, 3, 4]
g = (i * 2 for i in l)

print(type(g)) # <class 'generator'>
print(list(g)) # [2, 4, 6, 8]

ちなみに ( ) なしで定義しようとすると SyntaxError が出ました。 この g というやつは generator というオブジェクトだったようです。 遊んでいたときは「 l の次は g だよな(※ m です)」と思って変数名を g にしていたのですが、図らずしも頭文字を取っていたようです。

generator とはなんぞや

この generator というものについて軽く(軽~~く)調べると、Python において for 文で要素に順次アクセスできる対象である iterator とよく似ているが違うもののようです。

例えば上の g と同じものは次のように定義できます。

def my_generator(l):
    for i in l:
        yield i * 2

yield 文に出会うとそこで処理を抜けてデータ(今は要素を 2 倍したもの)を呼び出し元に返しますが、もう一度呼び出されると最後に抜けた場所に戻ります。簡単な例でこういうことです。

l = [1, 2, 3, 4]

def my_generator(l):
    for i in l:
        yield i * 2

gen = my_generator(l)

for i in gen:
    print(i)
    if i == 6:
        break

print('----')

for i in gen:
    print(i)

これを実行すると以下のようになります。

2
4
6
----
8

つまり gen は再び呼び出されたとき、最後に処理を抜けた場所を覚えていてそこに戻っています。 もっともここの for 文でループさせているときに毎回次の要素を取り出せているのも理屈は同じですが…

まぁ本には載っている

というわけで Python 入門書であるところの Python チュートリアル (同期にもらった)を開いてみると、見事に載ってました。 ジェネレータ式と呼ぶそうです。説明も

これはリスト内包表記によく似た構文で、角カッコの代わりに丸カッコを使う。(113ページ)

というシンプル極まりない記述です。

何が嬉しいのか気になりましたが、各要素が呼び出されるまで「リスト全体」を変数として持たない分メモリには優しいそうです。 「append() より list 内包表記」という話はありますがそれをさらに推し進めた感じになるのでしょうか? ちゃんと検証してみたいです。

おまけ

[ ] でも ( ) でもなく { } ならどうだ!と思って試してみました。

l = [1, 1, 2, 2, 4, 2, 3, 1, 3]
g = {i * 2 for i in l}

print(g)
print(type(g))

元から知ってる人も変換前の list のラインナップで「あっ(察し)」となった人も多いでしょうが結果はこうなりました。

{2, 4, 6, 8}
<class 'set'>

はい、set になりました。 重複を取り除くやつです。