タートルグラフィックスで学ぶ Python のキホン 第 15 章
教材コード
第 15 章

発展:再帰でえがく

自分で自分を呼ぶ関数.8 行のプログラムで,木が生えます.

第 13 章で関数を作れるようになり,第 14 章で return を習いました. このページは,その 2 つを済ませた人へのごほうびです. 関数の使い方をもうひとつだけ覚えると, びっくりするほど短いプログラムで,びっくりするほど複雑な絵が描けます.

その使い方とは,関数の中から,その関数自身を呼ぶというものです. これを再帰(さいき,recursion)といいます. はじめて聞くと「そんなことをして大丈夫なのか」と思うでしょう. 大丈夫です.ただしひとつだけ守らなければならない約束があります. それが何かも,このページで説明します.

タートルグラフィックスと再帰は,とても相性のよい組み合わせです. 木・雪の結晶・海岸線・シダの葉など,自然界にある 「一部を拡大すると全体と同じ形が出てくる」かたちが, 数行のプログラムで描けてしまいます. 少し進んだ内容ですが, ここまで進んだ人なら必ず読めます.

この章でできるようになること
  • 再帰とは何か,身近なたとえで説明できる
  • 止まる条件(ベースケース)が必要な理由がわかり,自分の関数に必ず書ける
  • 呼び出しが積み重なって,逆順に戻ってくる様子を追える
  • 「大きい問題を,同じ形の小さい問題に減らす」という考え方で関数が書ける
  • 枝分かれする木・コッホ曲線・シェルピンスキーの三角形を描ける
  • for で書くべき場面と,再帰が向く場面を使い分けられる
  • 深さを 1 増やすと命令数が何倍になるかを見積もれる
このページについて このページは発展の内容です.急いで読む必要はありません. そのかわり,読み終えたときには 「関数を習ってよかった」と思えるはずです.
  • 先に第 13 章(関数をつくる)第 14 章(値を返す関数)を済ませてください.def・引数・return を使います.
  • 第 13 章の約束「描き終わったら元の位置・元の向きに戻す」が,ここでは決定的に効いてきます.
  • 絵が出るまでに時間がかかることがあります.「速さ」を「いちばん速い」にして実行してください.

15.1 自分を呼ぶ関数

合わせ鏡を見たことがあるでしょうか. 鏡と鏡を向かい合わせると,鏡の中に鏡が映り, その鏡の中にもまた鏡が映って,奥へ奥へと続いていきます.

身のまわりには,こういう「中に自分と同じものが入っている」 かたちがたくさんあります.

  • 合わせ鏡 鏡の中に,鏡が映っている.
  • マトリョーシカ(入れ子人形) 人形を開けると,ひとまわり小さい人形が出てくる.それを開けると,また小さい人形が出てくる.
  • フォルダ フォルダの中にフォルダがあり,その中にもフォルダがある.
  • 木の枝 枝の先が分かれて,その先もまた分かれている.小枝だけを見ると,木全体を小さくしたような形をしている.

プログラムでこれを表すと,関数の中で,その関数自身を呼ぶという形になります.

自分を呼ぶ関数(※このままでは止まりません)
def matryoshka(size):
    # ここで自分自身を呼んでいる
    matryoshka(size - 1)

ただし,これだけでは永遠に止まりません. マトリョーシカにも,いちばん内側の 「もう開かない人形」がありますね. それに当たるものをプログラムにも書かなければいけません. そこが,再帰でいちばん大事なところです.

15.1.1 いちばん小さい例 — カウントダウン

絵はあとにして,まず print だけの,いちばん小さい再帰を見ます. 3 から 1 まで数えて「発射!」と言うプログラムです.

countdown.py
def countdown(n):
    if n == 0:               # 止まる条件
        print("発射!")
        return               # ここで終わり.自分を呼ばない
    print(n)
    countdown(n - 1)         # 1 つ小さくして,自分を呼ぶ

countdown(3)

コンソールに次のように出ます.

実行結果
3
2
1
発射!

countdown(3) を 1 回呼んだだけなのに, 4 行表示されました.何が起きたのか,順に追ってみましょう.

  1. countdown(3) n は 3.0 ではないので 3 と表示し,countdown(2) を呼ぶ.
  2. countdown(2) 2 と表示し,countdown(1) を呼ぶ.
  3. countdown(1) 1 と表示し,countdown(0) を呼ぶ.
  4. countdown(0) n が 0 になった.発射! と表示し,自分を呼ばずに return する.
  5. ここから,呼ばれたのと逆の順番に戻っていく.countdown(0)countdown(1)countdown(2)countdown(3) → 本文.

絵にすると次のようになります. 呼ぶたびに右下へ一段ずつ深くなり, 止まる条件にぶつかったところで折り返して, 深いところから順に戻ってくるのが分かります.

countdown(3)3 を表示 → countdown(2)countdown(2)2 を表示 → countdown(1)countdown(1)1 を表示 → countdown(0)countdown(0)止まる条件.呼ばずに戻る1. 呼ぶたびに 深くなる2. 深いほうから 逆順に戻る
図 15.1 countdown(3) の動き.呼び出しが積み重なり,止まる条件で折り返して,逆順に戻ってくる.
4 つの n は別々のもの ここで大事なのは,countdown4 つとも同時に動いているということです. countdown(3) は「countdown(2) が終わるのを待っている」状態で まだ生きています. そしてそれぞれが自分だけの n を持っています (第 13 章で習ったローカル変数です). だから n が 3・2・1・0 と,取り違えられずに残っているのです.

15.1.2 「行き」と「帰り」がある

さきほどのプログラムでは,戻ってくるときには何もしていませんでした. countdown を呼んだあとにも命令を書いてみましょう.

countdown_back.py
def countdown(n):
    if n == 0:
        print("発射!")
        return
    print("行き", n)
    countdown(n - 1)
    print("帰り", n)         # 戻ってきてから実行される

countdown(3)

結果はこうなります.「行き」は 3・2・1 の順,「帰り」は 1・2・3 の順です.

実行結果
行き 3
行き 2
行き 1
発射!
帰り 1
帰り 2
帰り 3

自分を呼ぶ行より前に書いた命令は「行き」に, あとに書いた命令は「帰り」に実行される. これは絵を描くときにそのまま効いてきます. たとえばあとで出てくる木では, 「行き」で枝を伸ばし,「帰り」でもとの場所まで戻るのに使います.

15.1.3 止まる条件が無いとどうなるか

では,止まる条件(if n == 0: の部分)を消すとどうなるでしょうか. 予想してから,実行してみてください.

no_base_case.py
# 止まる条件が無い.わざとエラーを出す例

def countdown(n):
    print(n)
    countdown(n - 1)

countdown(3)

3, 2, 1, 0, -1, -2, ... と数がどこまでも減っていき, しばらくして次のエラーで止まります.

エラーの内容
RecursionError: maximum recursion depth exceeded

「再帰の深さが限界を超えました」という意味です. 関数を呼ぶたびに,コンピュータは 「どこへ戻ればよいか」「n はいくつだったか」を覚えておく必要があります. 覚えておく場所には限りがあるので, だいたい 1000 回くらい深くなったところで 「もう覚えきれません」と音を上げるのです.

再帰の 2 つの部品 再帰関数には,必ず次の 2 つを書きます.
  1. 止まる条件(ベースケース).自分を呼ばずに終わる場合を,if で最初に書く.例:if n == 0: returnif length < 10: return
  2. 小さくして呼ぶこと.自分を呼ぶときは,引数を必ず止まる条件へ近づける.例:countdown(n - 1)tree(length * 0.7)countdown(n) のようにそのまま渡したら,永遠に止まりません.
どちらが欠けても RecursionError になります. 自分の再帰関数を書いたら,この 2 つがあるか必ず確かめてください
無限ループとのちがい while True: の無限ループと似ていますが,出るエラーは違います. 無限ループは「実行が 10 秒を超えました」, 止まらない再帰は RecursionError です. エラーの名前を見れば,どちらで困っているのかが分かります.

15.2 小さい同じ問題に減らす

再帰をうまく使うコツは, 大きい問題を,同じ形の小さい問題に減らすと考えることです. 自分で全部やろうとしないのがポイントです. ひと手間だけ自分でやって,残りは「小さい自分」に任せるのです.

15.2.1 階乗

階乗とは,1 からその数までを全部かけたものです. 5! = 5 × 4 × 3 × 2 × 1 = 120 のように書きます. ここで,かけ算の後ろのほうをよく見てください.

大きい問題の中に,小さい同じ問題が入っている
5! = 5 × 4 × 3 × 2 × 1
          ~~~~~~~~~~~~~
          これは 4! そのもの

つまり 5! = 5 × 4! です.同じように 4! = 4 × 3!3! = 3 × 2!…と続き,最後は 1! = 1 で止まります. この 2 行を,そのままプログラムにできます.

factorial.py
# n の階乗を返す
def fact(n):
    if n == 1:               # 止まる条件
        return 1
    return n * fact(n - 1)   # 自分より 1 小さい階乗に任せる

print("3! =", fact(3))
print("5! =", fact(5))
print("10! =", fact(10))

3! = 65! = 12010! = 3628800 と表示されます. fact の中身は 3 行しかありません. 「n が 1 なら 1」「そうでなければ n かける n-1 の階乗」.数学の定義をそのまま書いただけです.

return で値が戻ってくる 第 14 章で習ったとおり,return値を返して,その場で関数を抜ける命令です. return n * fact(n - 1) は, 「fact(n-1) を呼んで,返ってきた値に n をかけて, それを自分の呼び出し元に返す」という意味になります. 値が帰り道を通って,下から上へ運ばれていくわけです.
虫食い虫食い 15-A 1 から n までの合計

1 から n までを全部足した合計を返す関数 total を, 再帰で書きます. 1 + 2 + 3 + 4 + 5 の後ろのほうは 1 + 2 + 3 + 4,つまり total(4) そのものですね. ____ を埋めてください.

ヒント 止まる条件は n が 0 のときにしましょう (0 までの合計は 0 です). 自分を呼ぶときは 1 小さくして渡します.
fillr_total.py
# 1 から n までの合計を返す
def total(n):
    if n == ____:            # 止まる条件
        return 0
    return n + total(____)   # 1 小さい合計に任せる

print("1〜5 の合計:", total(5))
print("1〜10 の合計:", total(10))
print("1〜100 の合計:", ____)

15.3 タートルで描く — やさしいものから

ここからが本題です.まずは,枝分かれしないやさしい図形から始めます. 自分を呼ぶ回数が 1 回だけなら,動きは for とほとんど同じで,追いかけやすいからです.

15.3.1 入れ子の正方形

正方形の中に,ひとまわり小さい正方形.その中にまた小さい正方形. マトリョーシカそのものです.考え方はこうです.

  1. 1 辺 size の正方形を描く(これがひと手間).
  2. 少し内側へ入る.
  3. ひとまわり小さい入れ子の正方形を,自分に任せる.
  4. 入った分だけ戻って,位置と向きを元どおりにする(第 13 章の約束).
  5. ただし size が小さくなりすぎたら,何もしないで終わる(止まる条件).
nest_square.py
from turtle import *

pensize(2)
pencolor("royalblue")
ht()

# 1 辺 size の正方形から始まる入れ子模様を描く
def nest(size):
    if size < 20:            # 止まる条件
        return
    for i in range(4):       # ひと手間:正方形を 1 つ描く
        fd(size)
        lt(90)
    pu()                     # 内側へ 12 だけ入る
    fd(12)
    lt(90)
    fd(12)
    rt(90)
    pd()
    nest(size - 24)          # 残りは小さい自分に任せる
    pu()                     # 入った分だけ戻る
    lt(90)
    bk(12)
    rt(90)
    bk(12)
    pd()

pu()
goto(-100, -100)
seth(0)
pd()
nest(200)

done()
図 15.2 入れ子の正方形.nest は自分を 1 回だけ呼んでいる.

nest(200) と 1 回呼ぶだけで, 200 → 176 → 152 → … → 32 と 8 つの正方形が描かれました. 最後の nest(8)size < 20 なので,何も描かずに戻ります.

戻す処理を消してはいけない 最後の 6 行,「入った分だけ戻る」を消すとどうなるか,試してみてください. 絵は同じように見えます.入れ子の正方形はこれで完成しているからです. ところが,この nest部品として 2 回呼ぶと, 2 つ目がずれた場所に描かれてしまいます. 第 13 章の「描き終わったら元の位置・元の向きに戻す」は, 再帰ではとくに大事です. 再帰関数は,自分で自分を部品として使う関数だからです.
虫食い虫食い 15-B 入れ子の三角形

nest をまねて,入れ子の正三角形を描きます. 正三角形は 120 度ずつ 3 回曲がるのでしたね(第 2 章). ____ を埋めてください. 止まる条件と,自分を呼ぶ行の 2 か所がポイントです.

fillr_nest3.py
from turtle import *

pensize(2)
pencolor("seagreen")
ht()

# 1 辺 size の正三角形から始まる入れ子模様を描く
def nest3(size):
    if size < ____:          # 止まる条件
        return
    for i in range(3):       # ひと手間:正三角形を 1 つ描く
        fd(size)
        lt(____)
    pu()                     # 内側へ入る
    fd(20)
    lt(90)
    fd(11)
    rt(90)
    pd()
    ____(size - 40)          # 残りは小さい自分に任せる
    pu()                     # 入った分だけ戻る
    lt(90)
    bk(11)
    rt(90)
    bk(20)
    pd()

pu()
goto(-140, -120)
seth(0)
pd()
nest3(280)

done()

15.3.2 渦巻き

次は渦巻きです.「少し進んで,少し曲がって, あとは少し短い渦巻きを描く」.それだけです.

spiral_rec.py
from turtle import *

pensize(2)
pencolor("crimson")
ht()

# 長さ length から始まる渦巻きを描く
def spiral(length):
    if length < 4:           # 止まる条件
        return
    fd(length)
    lt(91)
    spiral(length - 3)       # 少し短い渦巻きは,小さい自分に任せる

spiral(160)

done()
図 15.3 再帰で描いた渦巻き.関数の中身は 5 行.

spiral は 53 回自分を呼んでから止まります. この図形は for でも書けます(第 13 章の虫食い 7-C がそれでした). どちらがよいかは 15.7 節で考えます.

角度を変えてみる 曲がる角度を lt(91) ではなく lt(120)lt(144) にすると,まったく違う模様になります. lt(89)lt(92) なども試してみてください. たった 1 度の違いで見た目が大きく変わります.

15.4 枝分かれする木

ここからが再帰の本領です. いままでは自分を1 回呼んでいました. 2 回呼ぶと,何が起きるでしょうか.

木を思い浮かべてください.幹があり,先で 2 つに分かれます. 分かれた枝も,その先でまた 2 つに分かれます. 枝 1 本だけを取り出して眺めると, それ自体が小さな木になっています. だから「木を描く」手順はこう書けます.

  1. length だけまっすぐ進む(これが幹).
  2. 左を向いて,ひとまわり小さい木を描く(自分に任せる).
  3. 右を向いて,ひとまわり小さい木を描く(もう一度自分に任せる).
  4. 向きを戻し,幹の長さだけ後ろへ下がって元の場所へ帰る.
  5. ただし length が短くなりすぎたら,何も描かずに終わる.
tree.py
from turtle import *

pensize(2)
pencolor("saddlebrown")
ht()

# 幹の長さ length の木を描く(描き終わったら元の位置・向きに戻る)
def tree(length):
    if length < 10:          # 止まる条件
        return
    fd(length)               # 幹を伸ばす
    lt(30)
    tree(length * 0.7)       # 左の枝は,小さい木に任せる
    rt(60)
    tree(length * 0.7)       # 右の枝も,小さい木に任せる
    lt(30)                   # 向きを戻す
    bk(length)               # 元の場所まで下がる

pu()
goto(0, -170)
seth(90)                     # 上を向かせる
pd()
tree(110)

done()
図 15.4 8 行の関数で描いた木.

8 行です.fdlt を 何百個も並べて描いた絵ではありません. 「木とは,幹の先に小さい木が 2 本ついたもの」と書いただけで,この形が出てきます.

なぜ最後に bk(length) が要るのか lt(30)tree(...)rt(60)tree(...)lt(30) の並びに注目してください. 回した角度の合計は +30 - 60 + 30 = 0 です. つまり向きが元に戻っています. 最後の bk(length) で位置も元に戻ります. 元の位置・元の向きに戻す関数だから,自分自身を安心して呼べるのです. 第 13 章の約束が,ここでいちばん強く効いています.

枝分かれがどれくらいの速さで増えるかを見てみましょう. 止まる条件を「長さ」ではなく「あと何回分かれるか」に変えて, 深さを指定できるようにします.

深さを引数にした木
# 深さ n の木を描く
def tree(length, n):
    if n == 0:               # あと 0 回なら,もう分かれない
        return
    fd(length)
    lt(30)
    tree(length * 0.7, n - 1)
    rt(60)
    tree(length * 0.7, n - 1)
    lt(30)
    bk(length)
深さ 2(枝 3 本)
深さ 4(枝 15 本)
深さ 7(枝 127 本)

深さを 1 増やすだけで,枝の数はほぼ 2 倍になります. 深さ 2 で 3 本,深さ 4 で 15 本,深さ 7 で 127 本. プログラムは一文字も変えていません.変えたのは数字 1 つだけです.

虫食い虫食い 15-C 木を完成させる

木のプログラムの ____ を埋めてください. ポイントは 3 つです.

  • 止まる条件で,自分を呼ばずに終わること
  • 自分を呼ぶときは,必ず短くして渡すこと
  • 最後に向きと位置を元に戻すこと(回した角度の合計を 0 にし,進んだ分だけ下がる)
fillr_tree.py
from turtle import *

pensize(2)
pencolor("darkgreen")
ht()

# 幹の長さ length の木を描く
def tree(length):
    if length < 12:
        ____                 # 自分を呼ばずに終わる
    fd(length)
    lt(25)
    tree(length * ____)      # 左の枝(必ず短くする)
    rt(50)
    tree(length * 0.75)      # 右の枝
    lt(25)                   # 向きを元に戻す
    ____(length)             # 位置を元に戻す

pu()
goto(0, -180)
seth(90)
pd()
tree(100)

done()

15.4.1 枝の太さと色を変える

本物の木は,根元が太くて先が細く,先のほうは緑色です. length の値を見て太さと色を決めれば,そのとおりになります. 引数が 1 つあるだけで,深さに応じた変化が自然につけられるのが再帰のよいところです.

tree_color.py
from turtle import *

ht()

# 幹の長さ length の木を描く.細い枝ほど緑にする
def tree(length):
    if length < 8:
        dot(5, "yellowgreen")    # 枝の先に葉をつける
        return
    pensize(length / 12 + 1)     # 根元ほど太く
    if length < 25:
        pencolor("forestgreen")
    else:
        pencolor("saddlebrown")
    fd(length)
    lt(28)
    tree(length * 0.72)
    rt(56)
    tree(length * 0.72)
    lt(28)
    pensize(length / 12 + 1)
    bk(length)

pu()
goto(0, -180)
seth(90)
pd()
tree(120)

done()
図 15.5 太さ・色・葉をつけた木.足したのは 5 行だけ.
ペンの状態は戻らない bk(length) の前にもう一度 pensize を書いているのは, 小さい木を描いている間に太さが細く変えられてしまうからです. 関数から帰ってきたとき,ペンの太さや色は元に戻っていない ことに注意してください. 位置と向きは自分で戻していますが,ペンの状態は戻らないのです.

15.5 コッホ曲線とコッホ雪片

次はコッホ曲線です.考え方はとても単純で, 1 本の線を 4 本に置き換える,それだけです.

  1. まっすぐな線を 3 等分する.
  2. 真ん中の 1 つを取り去って,かわりにそこへ山(正三角形の 2 辺)を立てる.
  3. できた 4 本の線それぞれに対して,同じことをする.
深さ 0(線 1 本)
深さ 1(線 4 本)
深さ 2(線 16 本)
深さ 4(線 256 本)

プログラムにするとこうなります. 「線 1 本」が止まる条件,「4 本に置き換える」が再帰です.

koch.py
from turtle import *

pensize(2)
pencolor("royalblue")
ht()

# 長さ length,深さ n のコッホ曲線を描く
def koch(length, n):
    if n == 0:               # 止まる条件:まっすぐ引くだけ
        fd(length)
        return
    koch(length / 3, n - 1)  # 1 本目
    lt(60)
    koch(length / 3, n - 1)  # 2 本目(山の左側)
    rt(120)
    koch(length / 3, n - 1)  # 3 本目(山の右側)
    lt(60)
    koch(length / 3, n - 1)  # 4 本目

pu()
goto(-180, -40)
seth(0)
pd()
koch(360, 4)

done()
図 15.6 深さ 4 のコッホ曲線.関数は 10 行.
向きの合計は 0 koch は,最後に向きを戻していません. 描き終わったとき,かめは出発したときと同じ向きを向いているからです. +60 - 120 + 60 = 0 になっているのを確かめてください. 位置は当然,線の長さだけ進んだところにあります. これはこれで「決まった動き」なので,部品として安心して使えます.
虫食い虫食い 15-D コッホ曲線を完成させる

コッホ曲線の ____ を埋めてください. 線を 3 等分するので,渡す長さは length の何分の 1 でしょうか. また,山を作るときに曲がる角度は,正三角形の外角を考えます (左に 60 度上がって,右に 120 度折り返し,左に 60 度戻る).

fillr_koch.py
from turtle import *

pensize(2)
pencolor("darkviolet")
ht()

# 長さ length,深さ n のコッホ曲線を描く
def koch(length, n):
    if n == ____:                # 止まる条件
        fd(length)
        return
    koch(length / ____, n - 1)
    lt(60)
    koch(length / 3, ____)       # 深さを 1 減らす
    rt(____)
    koch(length / 3, n - 1)
    lt(60)
    koch(length / 3, n - 1)

pu()
goto(-180, -30)
seth(0)
pd()
koch(360, 3)

done()

15.5.1 コッホ雪片

コッホ曲線を 3 本,正三角形の形につなぐとコッホ雪片になります. koch はそのまま使い回せます. 第 13 章でやったように,できた部品を組み合わせるだけです.

snowflake.py
from turtle import *

pensize(2)
pencolor("steelblue")
ht()

def koch(length, n):
    if n == 0:
        fd(length)
        return
    koch(length / 3, n - 1)
    lt(60)
    koch(length / 3, n - 1)
    rt(120)
    koch(length / 3, n - 1)
    lt(60)
    koch(length / 3, n - 1)

# コッホ曲線 3 本で雪片を作る
def snowflake(length, n):
    for i in range(3):
        koch(length, n)
        rt(120)

pu()
goto(-130, 75)
seth(0)
pd()
snowflake(260, 3)

done()
図 15.7 コッホ雪片.koch を 3 回呼んだだけ.

snowflake 自身は再帰ではありません.ただの for です. 再帰の関数を,ふつうの関数の中から部品として使う. これができると,作れる絵が一気に広がります.

15.6 シェルピンスキーの三角形

最後はシェルピンスキーの三角形です. これも考え方は 1 行で言えます. 「大きい三角形とは,半分の大きさの三角形が 3 つ集まったもの」. 左下・右下・上の 3 か所に,半分の三角形を置くだけです.

移動には,第 13 章で作った jump(線を引かずに動いて,向きは元に戻す)を使います. 三角形の 1 辺の長さが size のとき, 左下 → 右下は「右へ size/2」, 右下 → 上は「左斜め上(120 度)へ size/2」, 上 → 左下は「左斜め下(240 度)へ size/2」です.

sierpinski.py
from turtle import *

pensize(1)
pencolor("crimson")
ht()

# 線を引かずに移動する(向きは元に戻す)
def jump(dist, angle):
    pu()
    lt(angle)
    fd(dist)
    rt(angle)
    pd()

# 1 辺 size の正三角形を描く
def triangle(size):
    for i in range(3):
        fd(size)
        lt(120)

# 1 辺 size,深さ n のシェルピンスキーの三角形を描く
def sierpinski(size, n):
    if n == 0:               # 止まる条件:三角形をひとつ描く
        triangle(size)
        return
    sierpinski(size / 2, n - 1)   # 左下
    jump(size / 2, 0)
    sierpinski(size / 2, n - 1)   # 右下
    jump(size / 2, 120)
    sierpinski(size / 2, n - 1)   # 上
    jump(size / 2, 240)           # 元の場所へ帰る

pu()
goto(-170, -150)
seth(0)
pd()
sierpinski(340, 4)

done()
図 15.8 深さ 4 のシェルピンスキーの三角形.小さい三角形が 81 個.
深さ 1(三角形 3 個)
深さ 2(三角形 9 個)
深さ 5(三角形 243 個)
3 つに共通する骨組み 木・コッホ曲線・シェルピンスキーの三角形. この 3 つは形も雰囲気もまるで違いますが,プログラムの骨組みはそっくりです.
  1. if止まる条件を書き,そこでは自分を呼ばない.
  2. 自分より小さい自分を,何回か呼ぶ(木は 2 回,コッホは 4 回,シェルピンスキーは 3 回).
  3. 呼ぶ合間に,移動と回転をはさむ.
  4. 終わったら元の位置・元の向きに戻す.
この骨組みさえ覚えれば,自分の模様を作るのはむずかしくありません.

15.7 再帰と繰り返しの使い分け

ここまで読むと「for はもう要らないのでは」と思うかもしれません. そんなことはありません. for で書けるものは for で書くほうがよいのです.

15.3.2 節の渦巻きは,for でも書けました.見比べてください.

再帰で書いた渦巻き
for で書いた同じ渦巻き

絵は同じです.行数もほとんど変わりません. こういう場合は for のほうが,読む人にとってやさしいプログラムです. for なら RecursionError の心配もありません.

では,再帰でなければ困るのはどんなときでしょうか. 枝分かれするときです. 木を for で描こうとしてみてください. 幹から 2 本,その先からまた 2 本…と, 枝の 1 本 1 本について「いまどこにいて,どちらを向いていたか」を 自分で全部覚えておかなければなりません.とても書けたものではありません.

こういうときは書き方
決まった回数,同じことを繰り返すfor正多角形,マス目,らせん,花びらを並べる
一直線に,だんだん小さくしていくどちらでもよい(for のほうが安全)渦巻き,入れ子の正方形
枝分かれする・自分の中に自分が何個も入る再帰木,コッホ曲線,シェルピンスキーの三角形
何回で終わるか,書く前には分からない再帰フォルダの中のフォルダをたどる,迷路を探索する
どちらでも書けるけれど 実は,再帰で書けるものは理屈のうえではすべて forwhile でも書けます.ただし枝分かれのある形では, 「あとで戻る場所」を自分でリストに覚えておく必要があり, プログラムはずっと長く,ずっと分かりにくくなります. 再帰は,その面倒をコンピュータに肩代わりさせる道具だと思ってください.

15.8 深さを増やすと命令数が爆発する

再帰でいちばん驚くのは, 数字を 1 つ増やしただけで,絵の細かさが何倍にもなることです. これは楽しい反面,危険でもあります.

深さ nコッホ曲線の線分の数(4 のn乗)木の枝の数(2 のn乗 − 1)
141
2163
3647
425615
64,09663
865,536255
101,048,5761,023
1216,777,2164,095

コッホ曲線の深さを 4 から 8 にすると,線の数は 256 本から 65536 本. 256 倍です. 深さ 10 なら百万本を超えます.

深さは小さいほうから試す この教材の演習環境には,暴走を防ぐための上限があります.
  • 描画命令が 12 万個を超えると,「描画命令が 120000 個を超えました」と出て止まります.
  • 実行が 10 秒を超えると,「実行が 10 秒を超えました」と出て止まります.
  • 再帰が約 1000 段より深くなると,RecursionError になります.
再帰の絵を描くときは,必ず小さい深さから試してください. コッホ曲線なら 1 → 2 → 3 → 4,木なら 3 → 5 → 7. 1 つずつ増やして,描けるところまで確かめるのが安全です. いきなり 10 と書くと,待たされたあげくエラーになります.
細かくしても見えない 画面の大きさにも限りがあります. コッホ曲線の深さを上げても,いちばん細かい部分は 1 ピクセルより小さくなって見えなくなります. この教材の画面なら,コッホ曲線は深さ 4〜5, シェルピンスキーの三角形は深さ 5〜6,木は深さ 8〜10 くらいが 「見てうれしい」上限です.それ以上は,待ち時間が増えるだけで絵は変わりません.

15.9 この章で出てきた書き方

書き方意味
def f(n): の中に f(...)再帰.関数の中から,その関数自身を呼ぶ
if n == 0:return止まる条件(ベースケース).ここでは自分を呼ばない
f(n - 1)引数を小さくして自分を呼ぶ.止まる条件へ必ず近づける
f(length * 0.7)長さを掛け算で減らす書き方.木のように「だんだん短く」したいとき
return(値なし)何も返さずに,その場で関数を抜ける.止まる条件でよく使う
return n * f(n - 1)小さい自分の答えを使って,自分の答えを作って返す(第 14 章)
f(size, n - 1)大きさと深さを別々の引数にする.深さで止まる条件を作ると回数を読みやすい
bk(length)(自分を呼んだあと)元の位置へ戻す.再帰では必ず書く
回した角度の合計を 0 にする元の向きへ戻す.例:lt(30)rt(60)lt(30)
よくある間違い
症状原因と直し方
RecursionError: maximum recursion depth exceeded止まる条件が無い/引数を小さくしていない/小さくする向きが逆(n + 1 を渡しているなど)
描画命令が 120000 個を超えました深さが大きすぎる.1 つ減らして試す(15.8 節)
実行が 10 秒を超えました同じく深さが大きすぎる.または while True: を書いている
何も描かれない止まる条件がすぐ成立している.if length < 10: なのに tree(5) と呼んでいないか
2 回目に呼ぶとずれる元の位置・元の向きに戻していない.bk を書いたか,回した角度の合計が 0 かを確かめる
だんだん細くならない・太さがおかしいpensizepencolor は関数から帰っても戻らない.帰ってきたところで設定し直す(15.4.1 節)
絵が画面からはみ出す最初の長さが大きすぎる.goto で描き始める位置も調整する

演習

どの演習にも出発点となるプログラムが入っています. 「▶ この演習を開く」で右側に読み込み, それを発展させて作品にしてください. 深さや長さは,小さいほうから 1 つずつ増やして試すこと.

発展のヒント 発展のしかたに迷ったら,次のどれかを試してください.
  • 曲がる角度を変える(1 度違うだけで別の模様になります)
  • 小さくする割合を変える(0.70.60.8 に)
  • 左右で違う角度・違う割合にして,非対称にする
  • 深さ(引数 n)で色や太さを変える
  • できた再帰関数を,for の中から何回も呼んで並べる
演習演習 15-1 再帰で渦巻き

再帰で渦巻きを描く spiral(length) があります. まずそのまま実行し,「速さ」をゆっくりにして, どんな順番で線が引かれるかを見てください. そのうえで発展させてください.

  • 曲がる角度を lt(91) から変えてみる(89120144 など)
  • 短くする量 3 を変えてみる
  • 色も引数で渡せるようにして,for で何本も重ねる
ヒント length を見て色を変えることもできます. if length > 100: のように書けば, 外側と内側で色の違う渦巻きになります.
演習演習 15-2 自分の木を育てる

枝分かれする木のプログラムです.深さを引数にしてあります. 深さ 3 から始めて,1 つずつ増やしてください. そのうえで,自分だけの木に育ててください.

  • 左右の角度を変える(lt(30)rt(60) の組み合わせ)
  • 左右で短くする割合を変えて,かたむいた木にする
  • 深さ n で色と太さを変える(根元は茶色く太く,先は緑で細く)
  • 枝を 2 本ではなく 3 本に増やす
  • dot で葉や花をつける
左右の割合を変えた木
枝を 3 本にした木
演習演習 15-3 コッホ雪片

コッホ雪片のプログラムです. snowflake(length, n)n1 から 1 つずつ増やして,形の変化を見てください. そのうえで発展させてください.

  • 山を立てる角度 60 を変えてみる(8090 など)
  • rt(120) の回数を変えて,四角い雪片・五角の雪片にする
  • 大きさと色を変えて,雪片をいくつも重ねる
  • begin_fillend_fill で中を塗る
ヒント snowflakerange(3)rt(120) は 「正三角形の形につなぐ」という意味です. range(4)rt(90) にすれば,正方形の形につながります.
演習演習 15-4 シェルピンスキーの三角形

シェルピンスキーの三角形のプログラムです. 深さ n を 1・2・3・4・5 と増やして, 小さい三角形が 3 倍ずつ増えていくことを確かめてください. そのうえで発展させてください.

  • 深さ n によって色を変える(if n == 1: など)
  • triangle の中を begin_fillend_fill で塗る
  • 三角形ではなく正方形で同じことをやってみる(4 すみに半分の大きさの正方形を置く)
  • jump の角度を少しずらして,くずれた模様にする
ヒント 深さ 6 だと三角形は 729 個になります.描けますが時間がかかります. 深さ 8 は 6561 個です.まず小さい深さで形を確かめてください.
自由演習 15-5 自分の再帰模様をつくる

最後は自由制作です. 自分だけの再帰模様を作ってください.条件は 4 つです.

  1. 自分自身を呼ぶ関数を 1 つ以上作ること
  2. その関数に止まる条件が書いてあること
  3. 自分を呼ぶときに,引数を必ず小さくしていること
  4. 関数の上に,何をする関数かを1 行コメントで書くこと
ヒント ゼロから考えるのが難しければ,出発点のプログラムを改造してください. これは「正方形の 4 すみに,半分の大きさの自分を置く」模様です. range(4)range(3)range(5) に, 9012072 に変えるだけで, まったく別の模様になります.深さは 3 から試してください.
出発点(深さ 3)
三角形にして深さ 4

まとめ

  • 再帰とは,関数の中からその関数自身を呼ぶこと.合わせ鏡やマトリョーシカのように,中に自分と同じものが入っている形を表せる.
  • 再帰関数には必ず止まる条件(ベースケース)を書く.そこでは自分を呼ばない.これが無いと RecursionError になる.
  • 自分を呼ぶときは,引数を必ず小さくして,止まる条件へ近づける.
  • 呼び出しは積み重なり,止まる条件で折り返して逆順に戻ってくる.自分を呼ぶ行よりが「行き」,あとが「帰り」の処理.
  • 考え方は「大きい問題を,同じ形の小さい問題に減らす」.ひと手間だけ自分でやって,残りは小さい自分に任せる.
  • タートルでは,元の位置・元の向きに戻す(第 13 章の約束)ことが決定的に大事.再帰関数は,自分で自分を部品として使う関数だから.
  • 枝分かれするものは再帰が向く(木・コッホ曲線・シェルピンスキー).for で書けるものは for で書くほうがよい.
  • 深さを 1 増やすと命令数は何倍にもなる.小さい深さから 1 つずつ試すこと.この教材は 12 万命令・10 秒で止まる.

たった 8 行の tree が木になり,10 行の koch が雪の結晶になりました. 短いプログラムが単純な絵しか描けないとは限らないのです. プログラミングのおもしろさは,まさにここにあります.

第 13 章で「関数は部品だ」と学びました. 再帰は,その部品が自分自身を部品として使うという, ひとひねりしただけの考え方です. それだけで,これほど世界が広がります. 気が向いたときに,自分の模様を育ててみてください.