Pythonのリスト内包表記で色々な数列を作ってみた

Pythonのリスト内包表記の細かい動作がようやく分かってきたので、ブログに書いておく。

基本

Google先生に聞いたら詳しい説明が出てくるので、簡単に。

リスト内包表記は、下記のように書くとリストが作れる記法です。

>>> [x for x in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

これだけだと微妙ですが、例えば

>>> [x*x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

とべき乗のリストを作ってみたり、

>>> [x for x in range(10) if x % 2 == 0]
[0, 2, 4, 6, 8]

とif文を使い、偶数だけのリストを作ったりもできます。

処理の順番は下記の順番のようです。*1

>>> [ (4) for (2) in (1) if (3) ]

(1), (2)・・・リストの値を一つずつ(2)の変数に入れる
(3) ・・・条件式がTrueなら(4)の処理を行う
(4) ・・・処理

実践的なリスト内包表記

さて、リスト内包表記が使えるようになったけど、実際に使うときはもう少し複雑なリストが欲しくなりますよね。
という訳で、より実践的なリストの例を考えてみました。

  • もちろん、リスト内包表記のみで作成する
    • >>> [... for ... in ... if ...]の形のみです
  • 事前にimportしておいたり、変数を用意するのは禁止
九九表(二次元リスト)
[ [1, 2, 3, ... ,9], [2, 4, ... , 18], ... , [9, 18, ... , 81] ]
素数
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
フィボナッチ数列
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]

注意)この下に答えを書いてしまうので、上記の数列を自分で作成してみたい人は「続きを読む」をクリックしないように

直接、このページに来た人用に、無駄に改行を入れてあります。















































九九表

これはリスト内包表記を2つ作らないといけないです。

  • 九九表のリスト内包表記の例
>>> [[x*y for x in range(1,10)] for y in range(1, 10)]
[[1, 2, 3, 4, 5, 6, 7, 8, 9],
 [2, 4, 6, 8, 10, 12, 14, 16, 18],
 [3, 6, 9, 12, 15, 18, 21, 24, 27],
 [4, 8, 12, 16, 20, 24, 28, 32, 36],
 [5, 10, 15, 20, 25, 30, 35, 40, 45],
 [6, 12, 18, 24, 30, 36, 42, 48, 54],
 [7, 14, 21, 28, 35, 42, 49, 56, 63],
 [8, 16, 24, 32, 40, 48, 56, 64, 72],
 [9, 18, 27, 36, 45, 54, 63, 72, 81]]

これは↓のように書き換えると、なんとなく分かるかも。

[ (     ) for y in range(1, 10) ]

評価の順番的に y が作られた後に ( ) の部分が処理される訳ですね。
このため、( )内で y を使ってもエラーになりません。

素数

普段はエラトステネスの篩(ふるい) - Wikipediaを使うと思いますが、リスト内包表記の中で再帰呼び出しは使えないため、ここは素直に「1とその数自身以外に正の約数がない(割り切れない)」という条件を使います。

  • 素数のリスト内包表記の例
>>> [x for x in range(2, 100) if len([y for y in range(2, x) if x % y == 0]) == 0]
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

今回はifのlen()の中だけに注目すると分かりやすくなる。

[y for y in range(2, x) if x % y == 0]

これは“xの1とx以外の約数”のリストを作っている。
このリストの“要素数(割り切れた数)が0”であれば素数である。つまり

if len([y for y in range(2, x) if x % y == 0]) == 0

がifの条件となる。
if文が分かれば、後は簡単ですよね。

フィボナッチ数列

フィボナッチ数列とは、「どの項もその前の2つの項の和となる数列」です。

[0, 1, 1, 2, 3, 5, 8, 13, ... ]
0
1
1 = 0 + 1
2 = 1 + 1
3 = 1 + 2
5 = 3 + 2
...

詳しくはフィボナッチ数 - Wikipediaを読んでください。

フィボナッチ数列を作るためには、再帰 or 変数が必要なのですが、
いくら考えても解法が思い浮かばず、「フィボナッチ数列は無理なのだろう」と考えていました。

と思ってたら、@tosikawaさんからリプライが。

>>> [(seq.append(seq[i-2] + seq[i-1]), seq[i-2])[1] for seq in [[0, 1]] for i in range(2, 20)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]

  _, ._
(;゚ Д゚) …フィボナッチがデキテル?!


という訳で、頑張って解読してみた。

まず、18回ループするようにカウンタを作っているっぽい。

for i in range(2, 20)

次にフィボナッチを入れるリストを作る。

for seq in [[0, 1]]

そして、“その前の2つの項の和”をappendでリストに追加している。

(seq.append(seq[i-2] + seq[i-1]), seq[i-2])[1]

ただ、リストのappendの戻り値がNoneのため、このままだと[None, None, ...]のリストになってしまう。
そこで、タプルにして、( (処理), (戻り値) )[1]と言う方法で値を返している。

これは、少しソースを弄って、seqの値を見ると分かるかも。*2

>>> [(seq.append(seq[i-2] + seq[i-1]), seq)[1] for seq in [[0, 1]] for i in range(2, 5)]
[[0, 1, 1, 2, 3], [0, 1, 1, 2, 3], [0, 1, 1, 2, 3]]
ここまでリスト内包表記を書いてみて

考えて、解法が見つかった時は凄く面白いし、lambda、組み込み関数、シーケンス型の勉強にもなりました。
ただ、やっぱり普通にfor文や関数の再帰呼び出しとかでリスト作った方がソースの可読性は良いのだろうなぁ。


リスト内包表記の使用は容量用法を守り、適度に使用するのが良さそうです。

*1: (1)と(2)の順番はちょっと自信ないけど・・・

*2: seqは全て同じインスタンス?。[ [0, 1, 1], [0, 1, 1, 2], [0, 1, 1, 2, 3] ]になると想像していたけど、違った。