もっと便利な道具

INPUT · スライド

自分を呼ぶ関数

01 / 07

同じ形の小さな問題に分ける

5 の階乗(1から5までのかけ算)を考えてみよう。

5 * 4 * 3 * 2 * 1 だね。ここで、こういう見かたもできる。

「5の階乗とは、5 かける 4の階乗のこと」

同じことを言っているのに、右側にまた「階乗」が出てきた。大きな問題が、ひとまわり小さい同じ形の問題に化けたんだ。

この見かたをそのままコードにするのが 再帰(さいき)だよ。

02 / 07

関数が自分を呼ぶ

関数の中で、自分自身を呼ぶ。それだけで再帰になるよ。

読みかたはこう。

  • n が 0 なら答えは 1(かけ算の出発点)
  • そうでなければ n かける「n - 1 の階乗」

kaijou の中に kaijou が出てくるのが気持ち悪いかもしれない。でも Python は気にしない。呼ばれた時点で中身が決まっていればいいからだよ。

def kaijou(n):    if n == 0:        return 1    return n * kaijou(n - 1)print(kaijou(5))

実行結果

120

03 / 07

終わる条件を先に書く

ここがいちばん大事なところ。必ず「もう自分を呼ばない」場合を用意すること。これを ベースケース と呼ぶよ。

上の例では if n == 0: return 1 がそれだね。ここに来れば、それ以上は自分を呼ばずに終わる。

書き忘れるとどうなるか。永遠に自分を呼び続けて、Python が止めてくれるよ。

def kaijou(n):    return n * kaijou(n - 1)kaijou(5)

これを動かすと RecursionError: maximum recursion depth exceeded になる。「自分を呼ぶ深さが限界を超えた」という意味だよ。

再帰を書くときは、ベースケースから書く。これを習慣にしよう。

04 / 07

呼ばれる順を目で見る

再帰は頭の中で追いにくい。だから第4章でやった print デバッグ の出番だよ。

関数の先頭で「いま n がいくつで呼ばれたか」を表示してみよう。だんだん小さくなって 0 で止まるのが見えるはずだ。

0 まで下がってから、こんどは逆向きに答えが組み上がっていく。1 → 1 → 2 → 6 の順だね。

def kaijou(n):    print(f"kaijou({n})")    if n == 0:        return 1    return n * kaijou(n - 1)print(kaijou(3))

実行結果

kaijou(3)
kaijou(2)
kaijou(1)
kaijou(0)
6

05 / 07

for で書いたほうが素直なことも多い

正直に言うと、階乗も合計も for で書いたほうが読みやすい。下のリストの合計も、for で回して足すほうがずっと素直だよね。

それでも再帰を学ぶのは、この発想でしか書けない問題があるからだ。

もうひとつ、再帰には弱点がある。呼ぶたびに「どこまで進んだか」を Python が覚えておく必要があるので、深くなるとメモリを食うんだ。Python では千回ちょっとで打ち止めになるよ。

まず for で書けないか考える。書けないなら再帰。この順番でいいよ。

def goukei(kazus):    if len(kazus) == 0:        return 0    return kazus[0] + goukei(kazus[1:])print(goukei([1, 2, 3, 4]))

実行結果

10

06 / 07

自分を2回呼ぶ

自分を呼ぶのは1回とは限らない。フィボナッチ数列は、前の2つを足していく数のならびだよ。

0, 1, 1, 2, 3, 5, 8, 13, ...

「n 番目は、n-1 番目と n-2 番目の和」。そのまま書けば再帰になるね。ベースケースは「2 より小さいならその数のまま」だよ。

これは再帰だからこそ短く書ける例。for で書くと、前の2つを覚えておく変数が要って少しごちゃつくんだ。

def fibo(n):    if n < 2:        return n    return fibo(n - 1) + fibo(n - 2)print(fibo(10))

実行結果

55

07 / 07

再帰の本領は階層構造

再帰がいちばん光るのは、中に同じものが入っているデータだよ。

フォルダの中のフォルダ、返信への返信、リストの中のリスト。こういう階層構造は、深さが決まっていないので for では回しきれない。2段なら二重ループ、3段なら三重ループ——では書ききれないよね。

下の hiraku は、入れ子のリストを平らにする関数だよ。中身がリストだったら自分を呼んでほどく、そうでなければそのまま足す。それだけで何段でも対応できる。

「深さを気にしなくていい」——これが再帰をわざわざ学ぶ理由だよ。

def hiraku(xs):    kekka = []    for x in xs:        if type(x) == list:            kekka += hiraku(x)        else:            kekka.append(x)    return kekkaprint(hiraku([1, [2, [3, 4]], 5]))

実行結果

[1, 2, 3, 4, 5]