発展:再帰でえがく
自分で自分を呼ぶ関数.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 まで数えて「発射!」と言うプログラムです.
def countdown(n):
if n == 0: # 止まる条件
print("発射!")
return # ここで終わり.自分を呼ばない
print(n)
countdown(n - 1) # 1 つ小さくして,自分を呼ぶ
countdown(3)
コンソールに次のように出ます.
3 2 1 発射!
countdown(3) を 1 回呼んだだけなのに,
4 行表示されました.何が起きたのか,順に追ってみましょう.
countdown(3)nは 3.0 ではないので3と表示し,countdown(2)を呼ぶ.countdown(2)2と表示し,countdown(1)を呼ぶ.countdown(1)1と表示し,countdown(0)を呼ぶ.countdown(0)nが 0 になった.発射!と表示し,自分を呼ばずにreturnする.- ここから,呼ばれたのと逆の順番に戻っていく.
countdown(0)→countdown(1)→countdown(2)→countdown(3)→ 本文.
絵にすると次のようになります. 呼ぶたびに右下へ一段ずつ深くなり, 止まる条件にぶつかったところで折り返して, 深いところから順に戻ってくるのが分かります.
countdown(3) の動き.呼び出しが積み重なり,止まる条件で折り返して,逆順に戻ってくる.countdown が
4 つとも同時に存在しているということです.
実行しているのはいちばん深い 1 つだけで,ほかは途中で待っています.
countdown(3) は「countdown(2) が終わるのを待っている」状態で
まだ生きています.
そしてそれぞれが自分だけの n を持っています
(第 13 章で習ったローカル変数です).
だから n が 3・2・1・0 と,取り違えられずに残っているのです.
15.1.2 「行き」と「帰り」がある
さきほどのプログラムでは,戻ってくるときには何もしていませんでした.
countdown を呼んだあとにも命令を書いてみましょう.
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: の部分)を消すとどうなるでしょうか.
予想してから,実行してみてください.
# 止まる条件が無い.わざとエラーを出す例
def countdown(n):
print(n)
countdown(n - 1)
countdown(3)
3, 2, 1, 0, -1, -2, ... と数がどこまでも減っていき,
しばらくして次のエラーで止まります.
RecursionError: maximum recursion depth exceeded
「再帰の深さが限界を超えました」という意味です.
関数を呼ぶたびに,コンピュータは
「どこへ戻ればよいか」「n はいくつだったか」を覚えておく必要があります.
覚えておく場所には限りがあるので,
だいたい 1000 回くらい深くなったところで
「もう覚えきれません」と音を上げるのです.
- 止まる条件(ベースケース).自分を呼ばずに終わる場合を,
ifで最初に書く.例:if n == 0: return,if length < 10: return. - 小さくして呼ぶこと.自分を呼ぶときは,引数を必ず止まる条件へ近づける.例:
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 行を,そのままプログラムにできます.
# 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! = 6,5! = 120,10! = 3628800 と表示されます.
fact の中身は 3 行しかありません.
「n が 1 なら 1」「そうでなければ n かける
n-1 の階乗」.数学の定義をそのまま書いただけです.
return は
値を返して,その場で関数を抜ける命令です.
return n * fact(n - 1) は,
「fact(n-1) を呼んで,返ってきた値に n をかけて,
それを自分の呼び出し元に返す」という意味になります.
値が帰り道を通って,下から上へ運ばれていくわけです.
1 から n までを全部足した合計を返す関数 total を,
再帰で書きます.
1 + 2 + 3 + 4 + 5 の後ろのほうは
1 + 2 + 3 + 4,つまり total(4) そのものですね.
____ を埋めてください.
n が 0 のときにしましょう
(0 までの合計は 0 です).
自分を呼ぶときは 1 小さくして渡します.
# 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 辺
sizeの正方形を描く(これがひと手間). - 少し内側へ入る.
- ひとまわり小さい入れ子の正方形を,自分に任せる.
- 入った分だけ戻って,位置と向きを元どおりにする(第 13 章の約束).
- ただし
sizeが小さくなりすぎたら,何もしないで終わる(止まる条件).
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()
nest は自分を 1 回だけ呼んでいる.nest(200) と 1 回呼ぶだけで,
200 → 176 → 152 → … → 32 と 8 つの正方形が描かれました.
最後の nest(8) は size < 20 なので,何も描かずに戻ります.
nest を部品として 2 回呼ぶと,
2 つ目がずれた場所に描かれてしまいます.
第 13 章の「描き終わったら元の位置・元の向きに戻す」は,
再帰ではとくに大事です.
再帰関数は,自分で自分を部品として使う関数だからです.
nest をまねて,入れ子の正三角形を描きます.
正三角形は 120 度ずつ 3 回曲がるのでしたね(第 2 章).
____ を埋めてください.
止まる条件と,自分を呼ぶ行の 2 か所がポイントです.
止まる条件は「1 辺が 30 より小さい」とします.
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 渦巻き
次は渦巻きです.「少し進んで,少し曲がって, あとは少し短い渦巻きを描く」.それだけです.
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()
spiral は 53 回自分を呼んでから止まります.
この図形は for でも書けます(第 13 章の虫食い 13-C がそれでした).
どちらがよいかは 15.7 節で考えます.
lt(91) ではなく lt(120) や
lt(144) にすると,まったく違う模様になります.
lt(89)・lt(92) なども試してみてください.
たった 1 度の違いで見た目が大きく変わります.
15.4 枝分かれする木
ここからが再帰の本領です. いままでは自分を1 回呼んでいました. 2 回呼ぶと,何が起きるでしょうか.
木を思い浮かべてください.幹があり,先で 2 つに分かれます. 分かれた枝も,その先でまた 2 つに分かれます. 枝 1 本だけを取り出して眺めると, それ自体が小さな木になっています. だから「木を描く」手順はこう書けます.
lengthだけまっすぐ進む(これが幹).- 左を向いて,ひとまわり小さい木を描く(自分に任せる).
- 右を向いて,ひとまわり小さい木を描く(もう一度自分に任せる).
- 向きを戻し,幹の長さだけ後ろへ下がって元の場所へ帰る.
- ただし
lengthが短くなりすぎたら,何も描かずに終わる.
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()
8 行です.fd と lt を
何百個も並べて描いた絵ではありません.
「木とは,幹の先に小さい木が 2 本ついたもの」と書いただけで,この形が出てきます.
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)
深さを 1 増やすだけで,枝の数はほぼ 2 倍になります. 深さ 2 で 3 本,深さ 4 で 15 本,深さ 7 で 127 本. プログラムは一文字も変えていません.変えたのは数字 1 つだけです.
木のプログラムの ____ を埋めてください. ポイントは 3 つです.
- 止まる条件で,自分を呼ばずに終わること
- 自分を呼ぶときは,必ず短くして渡すこと(左右とも同じ割合
0.75で短くする) - 最後に向きと位置を元に戻すこと(回した角度の合計を 0 にし,進んだ分だけ下がる)
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 つあるだけで,深さに応じた変化が自然につけられるのが再帰のよいところです.
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) # 帰ってきたら太さと色を設定し直す
if length < 25:
pencolor("forestgreen")
else:
pencolor("saddlebrown")
bk(length)
pu()
goto(0, -180)
seth(90)
pd()
tree(120)
done()
bk(length) の前にもう一度 pensize と pencolor を書いているのは,
小さい木を描いている間に,太さが細く,色が緑に変えられてしまうからです.
書かないと,戻るときに茶色の幹が細い緑の線で上塗りされます.
関数から帰ってきたとき,ペンの太さや色は元に戻っていない
ことに注意してください.
位置と向きは自分で戻していますが,ペンの状態は戻らないのです.
15.5 コッホ曲線とコッホ雪片
次はコッホ曲線です.考え方はとても単純で, 1 本の線を 4 本に置き換える,それだけです.
- まっすぐな線を 3 等分する.
- 真ん中の 1 つを取り去って,かわりにそこへ山(正三角形の 2 辺)を立てる.
- できた 4 本の線それぞれに対して,同じことをする.
プログラムにするとこうなります. 「線 1 本」が止まる条件,「4 本に置き換える」が再帰です.
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()
koch は,最後に向きを戻していません.
描き終わったとき,かめは出発したときと同じ向きを向いているからです.
+60 - 120 + 60 = 0 になっているのを確かめてください.
位置は当然,線の長さだけ進んだところにあります.
これはこれで「決まった動き」なので,部品として安心して使えます.
コッホ曲線の ____ を埋めてください.
線を 3 等分するので,渡す長さは length の何分の 1 でしょうか.
また,山を作るときに曲がる角度は,正三角形の外角を考えます
(左に 60 度上がって,右に 120 度折り返し,左に 60 度戻る).
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 章でやったように,できた部品を組み合わせるだけです.
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()
koch を 3 回呼んだだけ.snowflake 自身は再帰ではありません.ただの for です.
再帰の関数を,ふつうの関数の中から部品として使う.
これができると,作れる絵が一気に広がります.
15.6 シェルピンスキーの三角形
最後はシェルピンスキーの三角形です. これも考え方は 1 行で言えます. 「大きい三角形とは,半分の大きさの三角形が 3 つ集まったもの」. 左下・右下・上の 3 か所に,半分の三角形を置くだけです.
移動には,第 13 章で作った jump(線を引かずに動いて,向きは元に戻す)を使います.
三角形の 1 辺の長さが size のとき,
左下 → 右下は「右へ size/2」,
右下 → 上は「左斜め上(120 度)へ size/2」,
上 → 左下は「左斜め下(240 度)へ size/2」です.
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()
ifで止まる条件を書き,そこでは自分を呼ばない.- 自分より小さい自分を,何回か呼ぶ(木は 2 回,コッホは 4 回,シェルピンスキーは 3 回).
- 呼ぶ合間に,移動と回転をはさむ.
- 終わったら元の位置・元の向きに戻す.
15.7 再帰と繰り返しの使い分け
ここまで読むと「for はもう要らないのでは」と思うかもしれません.
そんなことはありません.
for で書けるものは for で書くほうがよいのです.
15.3.2 節の渦巻きは,for でも書けました.見比べてください.
絵は同じです.行数もほとんど変わりません.
こういう場合は for のほうが,読む人にとってやさしいプログラムです.
for なら RecursionError の心配もありません.
では,再帰でなければ困るのはどんなときでしょうか.
枝分かれするときです.
木を for で描こうとしてみてください.
幹から 2 本,その先からまた 2 本…と,
枝の 1 本 1 本について「いまどこにいて,どちらを向いていたか」を
自分で全部覚えておかなければなりません.とても書けたものではありません.
| こういうときは | 書き方 | 例 |
|---|---|---|
| 決まった回数,同じことを繰り返す | for | 正多角形,マス目,らせん,花びらを並べる |
| 一直線に,だんだん小さくしていく | どちらでもよい(for のほうが安全) | 渦巻き,入れ子の正方形 |
| 枝分かれする・自分の中に自分が何個も入る | 再帰 | 木,コッホ曲線,シェルピンスキーの三角形 |
| 何回で終わるか,書く前には分からない | 再帰 | フォルダの中のフォルダをたどる,迷路を探索する |
for や
while でも書けます.ただし枝分かれのある形では,
「あとで戻る場所」を自分でリストに覚えておく必要があり,
プログラムはずっと長く,ずっと分かりにくくなります.
再帰は,その面倒をコンピュータに肩代わりさせる道具だと思ってください.
15.8 深さを増やすと命令数が爆発する
再帰でいちばん驚くのは, 数字を 1 つ増やしただけで,絵の細かさが何倍にもなることです. これは楽しい反面,危険でもあります.
| 深さ n | コッホ曲線の線分の数(4 のn乗) | 木の枝の数(2 のn乗 − 1) |
|---|---|---|
| 1 | 4 | 1 |
| 2 | 16 | 3 |
| 3 | 64 | 7 |
| 4 | 256 | 15 |
| 6 | 4,096 | 63 |
| 8 | 65,536 | 255 |
| 10 | 1,048,576 | 1,023 |
| 12 | 16,777,216 | 4,095 |
コッホ曲線の深さを 4 から 8 にすると,線の数は 256 本から 65536 本. 256 倍です. 深さ 10 なら百万本を超えます.
- 描画命令が 12 万個を超えると,「描画命令が 120000 個を超えました」と出て止まります.
- 実行が 10 秒を超えると,「実行が 10 秒を超えました」と出て止まります.
- 再帰が約 1000 段より深くなると,
RecursionErrorになります.
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 かを確かめる |
| だんだん細くならない・太さがおかしい | pensize や pencolor は関数から帰っても戻らない.帰ってきたところで設定し直す(15.4.1 節) |
| 絵が画面からはみ出す | 最初の長さが大きすぎる.goto で描き始める位置も調整する |
演習
どの演習にも出発点となるプログラムが入っています. 「▶ この演習を開く」で右側に読み込み, 各演習の「評価の目安」の必須をすべて満たしてから,発展させて作品にしてください. どの演習でも,止まる条件と自分を呼ぶ行を自分で書くところが入っています. 深さや長さは,小さいほうから 1 つずつ増やして試すこと.
- 曲がる角度を変える(1 度違うだけで別の模様になります)
- 小さくする割合を変える(
0.7を0.6や0.8に) - 左右で違う角度・違う割合にして,非対称にする
- 深さ(引数
n)で色や太さを変える - できた再帰関数を,
forの中から何回も呼んで並べる
半径 r の円を 1 つ描き,残りは半径を 15 小さくした自分に任せる
再帰関数 rings(r) を,自分で書いてください.
15.3.1 節の nest と同じ骨組みです.
円は circle(r) で描けます(かめのいる場所から左回りに 1 周し,元の場所・元の向きに戻ります).
- 半径が 10 より小さくなったら(
r < 10),何も描かずに終わる. - 円を描いたら,線を引かずに上へ 15 進んでから小さい自分を呼ぶ.こうすると円の中心がそろい,同心円になる.
- 小さい自分から帰ってきたら,進んだ分だけ戻って元の位置・元の向きにする.
- 出発点のとおり
goto(-150, -100)からrings(100)を呼ぶと,半径 100・85・70・55・40・25・10 の 7 個の円が描ける. - これもやりましょう:
ringsの中で,半径rが 60 より大きい円は"crimson",60 以下の円は"royalblue"で描く(外側 3 個が赤,内側 4 個が青になる). - これもやりましょう:
goto(150, -100)・seth(0)のあとでrings(100)をもう 1 回呼び,1 つ目とまったく同じ同心円が右側に描けることを確かめる(元の位置・向きに戻せていないと,2 つ目がずれる).
circle(r) の円は,かめの左側に中心があります.
右を向いたかめが上へ 15 進むには lt(90)・fd(15)・rt(90) です.
色は,circle(r) の前に if で pencolor を選んでから描きます.
必須(すべて満たして正しく動けば 8)
ringsの最初に,if r < 10:とreturnで止まる条件を自分で書いているringsの中で,半径を 15 小さくしてrings自身を呼び,上へ 15 ずらした同心円が 7 個描けている- 半径が 60 より大きい円は赤(crimson),60 以下の円は青(royalblue)で描かれている
- 描き終わると元の位置・元の向きに戻り,
goto(150, -100)から呼んだ 2 つ目が1 つ目と同じ形で描けている
発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)
- ずらす量(15)も引数
dにし,rings(r, d)で半径もdずつ小さくして,dを変えても同心円のまま描けるようにしている ringsが描いた円の数を戻り値で返し(止まる条件で 0,それ以外は 1 + 小さい自分の戻り値),print(rings(100))で 7 と出ることを確かめている(第 14 章)- 止まる半径(10)も引数にし,半径・ずらす量・止まる半径から何個描けるかを先に計算して,描いた数と合うことを確かめている
必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6
枝分かれする木のプログラムです.深さを引数にしてあります. 深さ 3 から始めて,1 つずつ増やしてください. そのうえで,次の要件を満たす自分の木に育ててください.
- 左右で短くする割合を変えて,かたむいた木にする.左の枝は
length * 0.6,右の枝はlength * 0.8にする(角度はlt(30)・rt(60)・lt(30)のままで,回した角度の合計は 0). - 幹の長さ 80・深さ 7 で,
goto(-120, -170)から上向き(seth(90))に描く. - これもやりましょう:深さ
nで太さと色を変える.太さはpensize(n)(根元ほど太い),色はnが 2 以下なら"forestgreen",それより大きければ"saddlebrown".小さい木から帰ってきたところ(bk(length)の前)でも,太さと色を設定し直す(15.4.1 節). - これもやりましょう:止まる条件(
n == 0)のところでdot(6, "hotpink")を描いて,枝の先に花をつける. goto(130, -170)から,幹の長さ 55・深さ 6 の 2 本目の木を描き,2 本目もまっすぐ上に立っている(ずれていない)ことを確かめる.
bk(length) で戻るときに
先の枝の緑色で幹をなぞってしまい,茶色の幹が緑になります.
「太さと色を決める」部分を関数にしておくと,2 か所で同じことを書かずにすみます.
必須(すべて満たして正しく動けば 8)
- 左の枝を 0.6 倍,右の枝を 0.8 倍にした非対称の木になっていて,回した角度の合計は 0 のまま
- 深さ
nで太さ(pensize(n))と色(2 以下は緑,それより上は茶)が変わり,bk(length)の前でも設定し直しているので,幹が茶色のまま残っている - 止まる条件(
n == 0)のところで,dotで花を描いている treeを 2 回呼んで,goto(-120, -170)から幹 80・深さ 7,goto(130, -170)から幹 55・深さ 6 の 2 本の木を描き,2 本目がずれていない
発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)
- 左右の割合や開く角度も引数にし,回した角度の合計が 0 のまま,引数だけでいろいろな形の木が描ける
- 枝を 3 本に増やしても,向きと位置が元に戻るようにしている(回す角度の合計を 0 に保つ)
treeが花の数を戻り値で返すようにし,深さnのとき 2 のn乗個になることをprintで確かめている
必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6
コッホ雪片のプログラムです.
snowflake(length, n) の n を
1 から 1 つずつ増やして,形の変化を見てください.
そのうえで,次の 2 つを作ってください.
- 山を反対側(内側)に立てるコッホ曲線
koch_in(length, n)を,kochをまねて自分で書く.ltとrtを入れ替えればよく,koch_inの中ではkoch_in自身を呼ぶ. snowflakeに角の数kを引数で渡し,range(k)とrt(360 / k)でつなぐようにする.k = 3なら三角形,k = 4なら正方形の形に閉じる.- これもやりましょう:
snowflakeに,描き始めの位置x・y,色,山の向き(kochかkoch_inかをifで切りかえる引数)も足し,移動(goto・seth(0))と色の設定もsnowflakeの中で行うようにする.そのうえで次の 3 つの雪片を,snowflakeの呼び出し 3 行だけで描く.描き始め(向きは seth(0))使う曲線 角の数 k1 辺 深さ 色 (-290, 150)koch3 170 3 "steelblue"(-60, 150)koch4 120 2 "seagreen"(130, 150)koch_in4 130 3 "darkviolet"
snowflake の range(3) と rt(120) は
「正三角形の形につなぐ」という意味です.
range(4) と rt(90) にすれば,正方形の形につながります.
山の角度 60 を変えたいときは,lt(60) だけを変えると
向きの合計が 0 にならず形がくずれます.
lt(a)・rt(2 * a)・lt(a) のように,
合計が 0 のままになるように変えてください(15.5 節「向きの合計は 0」).
必須(すべて満たして正しく動けば 8)
koch_inを自分で書き,止まる条件と,深さを 1 減らしてkoch_in自身を 4 回呼ぶ行がある(山が内側に立つ)snowflakeが角の数kを引数にとり,range(k)とrt(360 / k)でつないでいる- 表の 3 つの雪片(
k = 3のkoch,k = 4のkoch,k = 4のkoch_in)が,指定の位置・大きさ・深さで閉じた形に描けている snowflakeが位置・色・山の向きも引数で受け取り,3 つの雪片をsnowflakeの呼び出し 3 行だけで描いている
発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)
- 山の角度を引数
aにし,lt(a)・rt(2 * a)・lt(a)で向きの合計を 0 に保っている kochとkoch_inを,向きの引数(1 か −1)を掛けた角度で曲がる 1 つの関数にまとめている- 深さ
nの雪片の線分の数(k× 4 のn乗)を,戻り値で数えるか計算して表示し,描いた形と合うことを確かめている
必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6
シェルピンスキーの三角形のプログラムです.
深さ n を 1・2・3・4 と増やして,
小さい三角形が 3 倍ずつ増えていくことを確かめてください.
そのうえで,三角形ではなく正方形で同じことをする再帰関数
dust(size, n) を自分で書いてください.
考え方は 1 行です.「1 辺 size の正方形とは,
4 すみに置いた 3 分の 1 の大きさの正方形 4 つが集まったもの」.
- 止まる条件(
n == 0)では,1 辺sizeの正方形を"seagreen"で塗って終わる.そうでなければ,大きさをsize / 3にした自分を 4 回呼ぶ. - 4 回の呼び出しの間は
jumpでつなぐ.左下 → 右下は右へsize * 2 / 3(jump(size * 2 / 3, 0)),右下 → 右上は上へ(角度90),右上 → 左上は左へ(角度180),最後に下へ(角度270)戻って,元の位置・元の向きにする. - 出発点の
sierpinskiの呼び出しはgoto(-290, -150)・sierpinski(260, 4)に変えて左に寄せ,右側のgoto(20, -120)・seth(0)からdust(240, 3)を描く. - これもやりましょう:
dustの深さを 0・1・2・3 と変えたとき,塗られる正方形がそれぞれ何個になるかを数え,プログラムの最後に# 深さ 0:○個,深さ 1:○個,…の形のコメントで書く.あわせて,深さ 4 なら何個になるかを,描かずに計算して書く(深さが 1 増えると何倍になるかを考える.15.8 節).
dust(240, 1):1 辺 80 の正方形が 4 すみにdust(240, 2):その 4 つが,それぞれまた 4 すみに分かれる- 止まる条件だけを書く.
triangleをまねて,1 辺sizeの正方形を塗るsquare(size)を作る(range(4)とlt(90).塗るのはfillcolor・begin_fill()・end_fill()).dust(240, 0)で 1 辺 240 の正方形が 1 つ塗られれば OK. - 自分を呼ぶ 4 行を足す.
sierpinskiの「自分を 3 回呼んでjumpでつなぐ」形を 4 回にし,jumpの距離と角度を上の値にする.dust(240, 1)で左の図(1 辺 80 の正方形が 4 すみ,size * 2 / 3は 160)になれば OK. dust(240, 2)・dust(240, 3)と 1 つずつ増やす.深さ 1 では正しいのに深さ 2 で形がくずれたら,最後のjump(size * 2 / 3, 270)(元の位置へ戻る)が抜けているか,距離を160のような決まった数で書いている.
sierpinski が自分を 3 回呼ぶので 3 倍ずつ増えた
(深さ 4 で 81 個.図 15.8)ことと比べて考えます.
深さ 6 のシェルピンスキーは 729 個で,描けますが時間がかかります.
必須(すべて満たして正しく動けば 8)
dustを自分で書き,止まる条件(n == 0で 1 辺sizeの正方形を seagreen で塗って終わる)があるdustの中で,大きさを 3 分の 1 にしてdust自身を 4 回呼び,jump(size * 2 / 3, …)でつないで,最後に元の位置・元の向きに戻っているgoto(20, -120)からdust(240, 3)を描き,小さい正方形のかたまりが 4 すみに分かれて並ぶ形になっている(sierpinskiは左に寄せて重ならない)- 深さ 0〜3 で塗られる正方形の数と,深さ 4 の個数が,コメントに正しく書いてある
発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)
sierpinskiに色の引数colを足し,自分を呼ぶ 3 行で左下"crimson"・右下"darkorange"・上"royalblue"を渡して,止まる条件で受け取ったcolで塗る(深さ 4 なら,いちばん小さい三角形が 3 つ 1 組で 3 色になる.nの値で色を分けても,描くのは止まる条件のところだけなので効かない)dustが塗った正方形の数を戻り値で返し(止まる条件で 1,それ以外は 4 つの戻り値の合計),printで表示して,コメントに書いた数と合うことを確かめている- 4 すみに加えて真ん中にも置く版を作り,深さが 1 増えると何倍になるかを先に予想して,戻り値や数えた結果で確かめている
必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6
最後は自由制作です. 自分だけの再帰模様を作ってください.条件は 4 つです.
- 自分自身を呼ぶ関数を 1 つ以上作り,出発点の
cornersとは違う模様にすること(出発点の数を 1 つ変えただけのものは不可) - その関数に止まる条件が書いてあること
- 自分を呼ぶときに,引数を必ず小さくしていること
- 関数の上に,何をする関数かを1 行コメントで書くこと
range(4) を range(3) や range(5) に,
90 を 120 や 72 に変えるだけで,
まったく別の模様になります.深さは 3 から試してください.
必須(すべて満たして正しく動けば 8)
- 出発点の
cornersとは違う模様を描く,自分を呼ぶ関数がある(出発点の数を 1 つ変えただけのものは不可) - その関数に止まる条件(
ifとreturn)が書いてある - 自分を呼ぶときに,引数(深さや大きさ)を必ず小さくしている
- 関数の上に,何をする関数かを 1 行コメントで書いている
発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)
- 描き終わると元の位置・元の向きに戻ることを,2 回呼んで同じ形になるかで確かめている
- 形を決める数(角度・割合・枝の数など)を引数にし,引数を変えるだけで別の模様が描ける
- 描いた線の本数を戻り値で返すか計算で求め,深さが 1 増えると何倍になるかを確かめている
必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6
まとめ
- 再帰とは,関数の中からその関数自身を呼ぶこと.合わせ鏡やマトリョーシカのように,中に自分と同じものが入っている形を表せる.
- 再帰関数には必ず止まる条件(ベースケース)を書く.そこでは自分を呼ばない.これが無いと
RecursionErrorになる. - 自分を呼ぶときは,引数を必ず小さくして,止まる条件へ近づける.
- 呼び出しは積み重なり,止まる条件で折り返して逆順に戻ってくる.自分を呼ぶ行より前が「行き」,あとが「帰り」の処理.
- 考え方は「大きい問題を,同じ形の小さい問題に減らす」.ひと手間だけ自分でやって,残りは小さい自分に任せる.
- タートルでは,元の位置・元の向きに戻す(第 13 章の約束)ことが決定的に大事.再帰関数は,自分で自分を部品として使う関数だから.
- 枝分かれするものは再帰が向く(木・コッホ曲線・シェルピンスキー).
forで書けるものはforで書くほうがよい. - 深さを 1 増やすと命令数は何倍にもなる.小さい深さから 1 つずつ試すこと.この教材は 12 万命令・10 秒で止まる.
たった 8 行の tree が木になり,10 行の koch が雪の結晶になりました.
短いプログラムが単純な絵しか描けないとは限らないのです.
プログラミングのおもしろさは,まさにここにあります.
第 13 章で「関数は部品だ」と学びました. 再帰は,その部品が自分自身を部品として使うという, ひとひねりしただけの考え方です. それだけで,これほど世界が広がります. 気が向いたときに,自分の模様を育ててみてください.
復習:虫食いで確かめる
コードの ____ を埋めてください.実行前に結果を予測し,実行後に問題文の条件と照合しましょう.assert は条件が成り立つかを調べ,成り立たないとエラーになります.
0まで減らしたら呼び出しを止めます.実行結果が3,2,1の3行になるようにしてください.
def countdown(n):
if n ____ 0:
____
print(n)
countdown(n - 1)
countdown(3)
1からnまでの和を再帰で返します.sum_to(4) が10になるようにしてください.
def sum_to(n):
if n == 0:
return 0
return n + sum_to(____)
print(sum_to(4))
assert sum_to(4) == 10