読者です 読者をやめる 読者になる 読者になる

Pythonとタートルグラフィックスによる(再帰)プログラミング教育処方案

この記事のウリ

Python+turtleで再帰を用い、フラクタル的なお絵描きの作例をそこそこ用意しました。

前半は初心者向けの簡単な図形から始めているので、フラクタルを見たい人は記事後半まで頑張ってスクロールしてください。もちろん調べればLOGOによるこの手の作例は無尽蔵に出てくるのですが、LOGOの文献は古いせいか検索しても埋もれてる感があったので個人的に蒸し返したかったのです。

はじめに

子供の教育向け言語というといまはScratchが広く使われているが、再帰的な構造を描出するコードを書くときの簡潔さにおいてはタートルグラフィックス(LOGO)に軍配が上がる、と勝手に思っている。入門者に再帰を教える意味は? と聞かれると苦しいが、コッホ曲線とか描けたら楽しいと思うんですよね。

ここではPython+turtleでお絵描きをやります。Pythonを選んだ理由は、(1)Pythonが広く普及した言語であり、覚えてしまえばどこでも通用すること.(2)turtleがビルトインされているので環境を用意するのが容易であるという点.が挙げられます。

他のLOGO処理系に比較して、学習環境としてPythonを選ぶ弱点としては、turtleモジュールを毎回importしなければいけなかったり、手続きの呼び出しにカッコが必要なのでいちいち冗長、という点、さらに最大の弱点としてturtleモジュールは対話的に動かせない*1、ということが挙げられるでしょうか。またPythonの文法学習にロックインされるという問題が指摘されるかもしれません。しかしながら、LOGOと同等の構造化を行うぶんには、Pythonの文法は比較的に簡単ということも難しいということもないと思います。

モチベーションとしては、Scratchのような補助輪抜きでいきなりPythonから始めても、割と楽に教育環境として使えんじゃないっすかね、という謎の天啓を受けたので記事を書き始めました*2

案といっても作品例をそこはかとなく難易度順に並べただけなんで、ここにはturtleの説明も具体的な指導案もないです。強いていうなら亀と蛇の気持ちになって教えればいけると思う。

ここより常体。

作例

に入る前にちょっとした注意を。

前提として

全ての作品はファイルの一行目で

from turtle import *

する。import *がお嫌いな場合は適宜必要な関数を引っ張ってくる感じで。適当に検索するとimport turtle; turtle.forward(100)とかimport turtle; t=turtle.Turtle(); t.forward(100)ということをしている記事があるのだが、タートルの作品は書き捨て前提だし、書き捨てで構わないと思うし、わざわざ冗長に書く意味はないと思う。トップレベルでforward(100)って書いて動いてくれた方が楽で嬉しいはずだ。

python turtle特有の注意点

  • タートルは初期状態では右を向いている(最初は上を向いている処理系が多いと思う)
  • 最後にdone()を呼ばないと描画終了と同時にウィンドウが落ちる

私のコードについて

私はpython turtleの初心者なのだが、python turtleのGUIウィンドウの大きさは、ディスプレイのサイズその他に依存しているっぽいことにこの記事を書き終えてから気づいた。コードは私の使っているパソコンが表示したウィンドウに収まるように適当にsetposなどしているので、他のパソコンでそのままコピペして動かしてもちゃんと表示されない可能性がある。というかこの記事を書き終わってからsetupでウィンドウサイズを指定できること、window_widthwindow_heightの存在に気づいたので、すいませんもうやる気がありません。作品は画像として記事内に載せているが、その点だけご容赦いただきたい。

簡単な図形

四角形

人に教える場合を考えると、最初のうちは

fd(100)
rt(90)
fd(100)
rt(90)
fd(100)
rt(90)
fd(100)
rt(90)

という感じでやるのだろうが、この記事では特に気にせず、Pythonの言語機能は断りなく普通に使う。

for _ in range(4):
    fd(100)
    rt(90)

done()

f:id:cofree:20170120135235p:plain

三角形

for _ in range(3):
    fd(100)
    rt(120)

done()

四角より書き方が分からないという人は多くなる。rtは何度で回転したらいい?って聞くと60度と間違える人は割といるのだ。

多角形

多角形の回転角が360/nで一般化されるということ。

from turtle import *

def polygon(n, length=120):
    angle = 360/n
    for _ in range(n):
        fd(length)
        rt(angle)

if __name__ == '__main__':
    lt(180)
    for i in range(5, 9):
        polygon(i)
    done()

f:id:cofree:20170120150142p:plain

ちょっと複雑な図形

n角形を描くだけだと面白くなく、さりとていきなりフラクタルであるとかL-Systemに進むと飛躍があるので、ちょっとした幾何学的模様を作る。

星型

亀が2回転するので720度を頂点数5で割って144度ずつ曲がるということ。教育的には亀が回転する様子を体で表現したい。

for _ in range(5):
    fd(150)
    rt(144)

done()

f:id:cofree:20170120145355p:plain

回りながらたくさん円を描く

どっかで見たような感じだが簡単に作れるんで…

口寂しいので色をつけてみたが、素人なので難しい。hsvを使えばそれっぽくなると思う。

speed(0)
pu()
setpos(0, 50)
pd()

pensize(4)

n = 30
angle = 360/n

for i in range(n):
    pencolor(((i/n), 0, 1.0-(i/n)))

    fd(10)
    rt(angle)
    circle(100)

done()

f:id:cofree:20170120180655p:plain:w300

オウムガイ

参考:Nautilus | Circle and Square

回転しながら徐々に大きな正方形を描くことでらせん形を作り出している。ただオウムガイとか自然界に現れるらせん形の比率ってフィボナッチだという話を聞きかじったので、指数でやるのはちょっと違うかも。

from turtle import *

def square(length):
    for _ in range(4):
        fd(length)
        rt(90)

def spiral(n, length, angle):
    for _ in range(n):
        square(length)
        rt(angle)
        length = length*1.05


if __name__ == '__main__':
    n = 100
    length = 3
    angle = 10
    speed(0)

    spiral(n, length, angle)
    done()

f:id:cofree:20170120224105p:plain

このへんもうちょっと面白い例がないかな、と思うが、思いつかないのでフラクタルに移る。

フラクタルでお絵かき

だいたいL-Systemだが、生成規則のようなものは使っていない。

2分木

どうやって描いているのか理屈が分かりやすいという点で、フラクタルの中では一番簡単だと思う。

平衡な木だけだとつまらないので傾いた木を作るところまでやるとグー。単純な規則から多様な形が生み出されることに気づく。

色々パラメータを弄れる関数にしてしまったが、都度手書きするくらいのほうが覚えるにはよい。

from turtle import *

def tree(n, angle=20, length=100, skew=0, r_ratio=3/4, l_ratio=3/4):
    if n < 0:
        return
    fd(length)
    rt(angle+skew)
    tree(n-1, angle, length * r_ratio)
    lt(angle * 2)
    tree(n-1, angle, length * l_ratio)
    rt(angle-skew)
    bk(length)

if __name__ == '__main__':
    lt(90)
    pu()
    bk(100)
    pd()
    speed(0)
    tree(8, 35, skew=25)
    done()

f:id:cofree:20170120173555p:plain

ありそうな木

分岐の仕方を増やすと実際にありそうな木(?)ができる。

def tree(length=200):
    if length <= 10:
        fd(length)
        bk(length)
        return

    fd(length*3/8)

    rt(30)
    tree(length * 2/3)
    lt(30)

    fd(length/4)

    lt(35)
    tree(length * 3/4)
    rt(35)

    fd(length/8)

    rt(25)
    tree(length/2)
    lt(25)

    fd(length/4)

    bk(length)

lt(90)
speed(0)
tree()
done()

f:id:cofree:20170120190404p:plain

参考:ロゴ情報室(pdf)

再帰部で亀さんが進む距離を調整すると味わい深い形状が生まれる。

from turtle import *

def forest(n, length=1000):

    if n <= 0:
        fd(length)
        return
    forest(n-1, length * 0.5)
    rt(85)
    forest(n-1, length / 3)
    lt(170)
    forest(n-1, length / 3)
    rt(85)
    forest(n-1, length * 0.35)

if __name__ == '__main__':
    pu()
    setpos(270, -20)
    pd()
    lt(180)
    speed(0)
    forest(5)
    done()

f:id:cofree:20170120171759p:plain

コッホ曲線/雪の結晶

線分を3等分し、中の線を元の線分に対してパキッと真ん中から60度の角度で折る(全体の長さは4/3倍になる)。そこからできたそれぞれの線分に同じ操作を繰り返し適用する。

コッホ曲線を4回転すると雪の結晶っぽくなる、というのも知ってる人は多いと思う。

コードについて言うと、何をしているのかという点では分かりやすいものの、実装はあまりよくないと感じる。再帰の深さに対し、線の長さがどれくらいだと画面に収まりが良くなるのか分からなかった。まあディスプレイの広さが有限という観点を忘れれば長さなんてどうでもいいのかもしれない(?)

from turtle import *

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

if __name__ == '__main__':
    n = 4
    length = 400
    pu()
    setpos(-200, 200)
    pd()
    speed(0)
    hideturtle()
    for _ in range(4):
        koch(n, length)
        rt(90)
    done()

f:id:cofree:20170120212505p:plain

LévyのC曲線

それぞれの線分を半分に割って「くの字形」に曲げる、という操作を繰り返すイメージ。ドラゴン曲線と構成が似ているのだが、描くのはこちらのほうが簡単である。

from turtle import *

def levy(n, length=400):
    if n <= 0:
        fd(length)
        return
    rt(45)
    levy(n-1, length * 2/3)
    lt(90)
    levy(n-1, length * 2/3)
    rt(45)

if __name__ == '__main__':
    speed(0)
    pu()
    setpos(-150, 50)
    pd()
    levy(10)
    done()

f:id:cofree:20170120164413p:plain

ドラゴン曲線

くの字とへの字がある(雑)。L-System的な規則を使わずに実装するなら相互再帰を使う…がDragon curve - Rosetta Codeにあるやり方なら不要になる。

from turtle import *


def dragon(n, length=300):
    dcr(n, length)

def dcr(n, length):
    length /= 1.414
    if n <= 0:
        fd(length)
        return
    rt(45)
    dcr(n-1, length)
    lt(90)
    dcl(n-1, length)
    rt(45)

def dcl(n, length):
    length /= 1.414
    if n <= 0:
        fd(length)
        return
    lt(45)
    dcr(n-1, length)
    rt(90)
    dcl(n-1, length)
    lt(45)


if __name__ == '__main__':
    pu()
    setpos(-100, 0)
    pd()
    speed(0)
    dragon(11)
    done()

f:id:cofree:20170120165902p:plain

ヒルベルト曲線(Hilbert curve)

空間充填曲線って言葉かっこいいよね!

コードはやや冗長。fdの回数が減らせそうにない気がした。ドラゴン曲線の項で紹介した記事にあるように、ltとrtを関数オブジェクトして再帰部に渡すと、相互再帰がなくなり綺麗に書けるが、ややPythonicなので好きな人がやればいいと思う。

ペンの太さをいじっておりちょっとずるい。

from turtle import *

def hilbert(n, length):
    lhlb(n, length)

def lhlb(n, length):
    if n <= 0:
        return
    rt(90)
    rhlb(n-1, length)
    fd(length)
    lt(90)
    lhlb(n-1, length)
    fd(length)
    lhlb(n-1, length)
    lt(90)
    fd(length)
    rhlb(n-1, length)
    rt(90)

def rhlb(n, length):
    if n <= 0:
        return
    lt(90)
    lhlb(n-1, length)
    fd(length)
    rt(90)
    rhlb(n-1, length)
    fd(length)
    rhlb(n-1, length)
    rt(90)
    fd(length)
    lhlb(n-1, length)
    lt(90)

if __name__ == '__main__':
    pu()
    setpos(-200, 200)
    pd()
    speed(0)
    pensize(4)
    hilbert(6, 6)
    done()

f:id:cofree:20170120155156p:plain

シェルピンスキ曲線(Sierpinski curve)

個人的に見ていて好きなパターンかもしれない。なぜかは分からないが。

from turtle import *

def sierpcurve(n, length=8, angle=45):
    if n <= 0:
        fd(length)
        return
    rt(angle)
    sierpcurve(n-1, length, -angle)
    lt(angle)

    fd(length)

    lt(angle)
    sierpcurve(n-1, length, -angle)
    rt(angle)

lt(90)
speed(0)
length=8
for _ in range(4):
    sierpcurve(7, length)
    rt(45)
    fd(length)
    rt(45)
done()

f:id:cofree:20170120201914p:plain f:id:cofree:20170120203134p:plain

格子曲線(Grate curve)

これも空間充填系だが動きがやや複雑。縦幅と横幅を交換しつつ(交互に縮小しながら)再帰するので頭がこんがらがった。

コードはdrawing space filling durve in logo(pdf)を書き写しているだけ。

from turtle import *

def two(a, c, w):
    if c < 1:
        return
    rt(a)
    fd(1)
    rt(a)
    fd(w)
    lt(a)
    if c > 1:
        fd(1)

    lt(a)
    fd(w)
    two(a, c-2, w)


def square(a, h, w):
    fd(w)
    two(a, h-1, w)

def grate(n, a=90, w=320, h=320):
    if n <= 0:
        square(a, h, w)
        return
    rt(a)
    grate(n-1, -a, h/4, w)
    fd(h/8)
    grate(n-1, a, h/4, w)
    fd(h/8)
    grate(n-1, -a, h/4, w)
    lt(a)

bgcolor("red")
pu()
setpos(-200, 150)
pd()
speed(0)
grate(4)
done()

f:id:cofree:20170120211002p:plain

クヌース曲線(Knuth curve)

これは規則が素直で分かりやすいと思う。

from turtle import *

def knuth(n, a=-90, t=45, h=8):
    if n == 0:
        rt(45+t)
        fd(h)
        lt(45+t)
        return
    rt(2*t + a)
    knuth(n-1, 2*t, -t, h)
    rt(45 - 3*t - a)
    fd(h)
    lt(45 - t + a)
    knuth(n-1, 0, -t, h)
    rt(a)

if __name__ == '__main__':
    pu()
    setpos(200, 0)
    pd()

    lt(180)
    
    speed(0)
    knuth(9, h=5)
    done()

f:id:cofree:20170120220134p:plain

(おまけ)ローレンツアトラクタ

フラクタルではない。再帰も使わない。

参考にしたのはLorenz system - Wikipediaだが、Lorenz attractor using turtle · ars.meもチラ見した。タイムステップとスケーリングどれくらいとったらええねーん、的なところでちょっと面倒になった詰まったので、カンニングした。その際手直ししたのでだいたい同じプログラムになった。うちの亀はspeed(0)しても描画が遅いので確認に時間がかかり不安になる*3atan2でどうして接方向が出るのか?という話になると、「うん…(説明が面倒)」となってしまうのでちょっと辛い。

from turtle import *
from math import atan2

def lorenz(sigma, beta, rho):
    dt = 0.01
    scale = 10
    x, y, z = (1, 1, 1)
    dx, dy = (0, 0)
    while True:
        setpos(x*scale, y*scale)
        setheading(atan2(dy, dx))
        
        dx = (sigma*(y-x)) * dt
        dy = (x*(rho-z)-y) * dt

        x += dx
        y += dy
        z += (x*y - beta*z) * dt


if __name__ == '__main__':
    speed(0)
    lorenz(10, 8/3, 28)

以下の画像はdt=0.02で動作確認したものをスクショしたのでやや粗い。

f:id:cofree:20170120143616p:plain

おわりに

再帰楽しいですね。LOGOの文献には忘れかけられている教育的資料が多数あるので掘ってみると面白いものが結構見つかります。最近はプログラミングスクールも増えているようですが(よく知らんけど)、自分が子供だったらScratchでしょうもないゲーム作らされるよりこういうの教わったらプログラミング好きになるだろうなぁ、と思ったのがこの記事の発端でした。

*1:repl.itなどウェブ上のPythonでturtleを動かすという手がありますが、やはりローカル環境が欲しいです

*2:私はひきこもりなので誰にも教える当てはないのですが

*3:YouTubeとかでturtleを使ってる動画を探すと明らかな描画測度の違いを感じた