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

関数をつくる

命令に名前をつけて,自分だけの部品を作ります.

ここまでで,みなさんは fdltiffor に加えて,たくさんのかめ・アニメーション・ゲームまで 作れるようになりました.これで「どんなものでも作れる」はずです. ところが実際に作ってみると,プログラムはどんどん長くなり, 自分でも何を書いたのか分からなくなってきたのではないでしょうか.

今回はその悩みに効く道具を習います.関数(かんすう)です. 関数とは,いくつかの命令をひとまとめにして,名前をつけたものです. 一度作ってしまえば,その名前を書くだけで中身がまるごと実行されます. fdlt と同じように使える, 自分専用の命令を作るということです.

Python では,これを関数といいます. def という 3 文字だけ覚えれば作れます.

この章でできるようになること
  • 同じコードの繰り返しに気づき,def で関数にまとめられる
  • 関数の定義呼び出しの違いが説明できる
  • 関数を呼んだとき,処理がどう動いて戻ってくるかを追える
  • 引数で「大きさ」「角の数」「色」を外から渡せる
  • 引数が書いた順番で決まることがわかり,名前=値(キーワード引数)でも渡せる
  • printlen も関数だと気づき,自分で作る関数との違いが説明できる
  • 関数の中の変数が外から見えないこと(ローカル変数)がわかる
  • 「描き終わったら元の位置・元の向きに戻す」を守った,組み合わせられる関数が書ける
第 10 章からの「決まり文句」の正体 じつは,defすでに 3 回も使っています. 第 10 章の def step():,第 11 章の def draw_board():, 第 12 章の def tick():def go_left():. あのときは「ontimer に渡すための決まり文句」として, 理由を説明しないまま書いてもらいました.

今回は,その正体を明かします. なぜ名前を付けるのか,かっこの中に並べた名前(引数)は何なのか, そして global は何をしていたのか. 決まり文句の理由が分かると,これまで書いてきたプログラムの見え方が変わります.

なぜ関数を作るのか 関数を作る目的は 3 つあります.旧来の教科書から変わらない,大事な 3 つです.
  1. 同じ処理をひとつにまとめる.同じことを 2 回書いたら,それは関数にする合図です.直すときも 1 か所で済みます.
  2. 名前をつけて読みやすくする.20 行の命令の列より,draw_house() の 1 行のほうが「何をしているか」がすぐ分かります.
  3. 部品として組み合わせる.小さな関数を組み合わせて大きな関数を作れば,プログラム全体の複雑さを抑えられます.これを構造化プログラミングといいます.

13.1 同じ絵を何度も描いている

まず,いまの自分のプログラムがどうなっているかを見てみましょう. 次は正方形を 3 つ並べて描くプログラムです.第 6 章で習った for を使って, 1 つぶんはすでに 3 行に縮めてあります.

three_squares.py
from turtle import *

pensize(2)

# 左の正方形
pu()
goto(-220, -40)
pd()
for i in range(4):
    fd(80)
    lt(90)

# 中央の正方形
pu()
goto(-120, -40)
pd()
for i in range(4):
    fd(80)
    lt(90)

# 右の正方形
pu()
goto(-20, -40)
pd()
for i in range(4):
    fd(80)
    lt(90)

done()
図 13.1 正方形を 3 つ.同じ 3 行が 3 回出てくる.

for i in range(4): から始まる 3 行が,まったく同じ形で 3 回出てきます. 第 2 章でも「同じ 8 行を 3 回書いていて,いかにも無駄です」と書きました. for で少し短くなりましたが,本質は変わっていません

このままだと困ること
  • 1 辺を 80 から 60 に変えたくなったら,3 か所直さなければなりません.1 か所直し忘れると,そこだけ大きさの違う変な絵になります.
  • 正方形ではなく正六角形にしたくなったら,やはり 3 か所です.
  • そもそも,このプログラムをぱっと見て「正方形を 3 つ描いている」と気づけるでしょうか.fdlt の列を読み解く必要があります.

同じものを 2 回以上書いたら,関数にする. これが今回のいちばん大事な合図です.

13.2 関数をつくる — def

13.2.1 def の書き方

さきほどのプログラムを,関数を使って書き直します. まず「正方形を描く」という関数を作り,それを 3 回呼びます.

square_func.py
from turtle import *

pensize(2)

# ここが「関数の定義」
def square():
    for i in range(4):
        fd(80)
        lt(90)

# ここが「関数の呼び出し」
pu()
goto(-220, -40)
pd()
square()

pu()
goto(-120, -40)
pd()
square()

pu()
goto(-20, -40)
pd()
square()

done()
図 13.2 図 13.1 とまったく同じ絵.中身は 1 か所にまとまった.

def は define(定義する)の略です.書き方の決まりは次の 3 つだけです.

  1. def のあとに関数の名前を書き,続けてかっこを書く(今は空のかっこ).
  2. その行の終わりにコロン : を打つ.
  3. 次の行から,関数の中身を字下げ(インデント)して書く.字下げが戻ったところで関数の定義は終わり.
関数定義の形
def 関数の名前():
    中身の命令
    中身の命令
コロンと字下げ コロンと字下げは,第 5 章の if,第 6 章の for と同じ決まりです. 「コロンを打ったら,次の行から字下げ」と覚えてください. 字下げは半角スペース 4 個に揃えます. エディタで Tab キーを押せば 4 個ぶん入ります.

13.2.2 定義と呼び出しは別物

ここがいちばん誤解しやすいところです. def を書いただけでは,中身は 1 回も実行されません. def は「こういう命令を作っておいてね」とコンピュータに登録するだけです.

define_only.py
from turtle import *

# 関数を定義しただけ.呼んでいない
def square():
    for i in range(4):
        fd(80)
        lt(90)

done()
図 13.3 何も描かれない.かめが中央にいるだけ.

実行しても,エラーは出ません.そのかわり何も描かれません. 料理でいえば,レシピを書いただけで台所に立っていない状態です. 最後に square() の 1 行を足してから,もう一度実行してみてください.

書き方呼び名意味
def square():定義(definition)square という命令を作る」.この時点では何も起きない
square()呼び出し(call)square を実行しろ」.ここで中身が動く
かっこを忘れない 呼び出しのときもかっこは必ず書きますsquare() と書けば実行されますが,square とかっこ無しで書いても, エラーにならないのに何も起きません(「関数そのもの」を指しているだけになります). 「動かないのになぜ?」というときは,まずかっこを確かめてください.

13.2.3 呼び出しの流れ — 呼ぶ・実行する・戻ってくる

関数を呼ぶと,プログラムの流れはいったん関数の中へ飛びます. そして関数の中身を最後まで実行したら, 呼んだ場所のすぐ次の行へ戻ってきます

本文(呼び出す側)fd(50)square()fd(50)関数の中身def square(): for i in range(4): fd(80) lt(90)1. 呼ぶ3. 戻ってくる2. 中を上から順に実行する
図 13.4 関数を呼んだときの流れ.呼ぶ → 中を実行 → 呼んだ場所へ戻る.

次のプログラムで,この流れを目で確かめましょう. 「速さ」を「とてもゆっくり」にして実行してください. 正方形を描く関数と,次の場所へ移動する関数を,交互に 3 回呼んでいます.

square_move.py
from turtle import *

pensize(2)

# 正方形を描く
def square():
    for i in range(4):
        fd(80)
        lt(90)

# 次に描く場所まで移動する
def move():
    pu()
    fd(100)
    pd()

pu()
goto(-220, -40)
pd()

for i in range(3):
    square()
    move()

done()
図 13.5 「描く」と「移動する」を交互に 3 回.本文は 3 行で済んでいる.

本文(関数の外)で実際にやっていることは, 「正方形を描く」「移動する」を 3 回繰り返す,ただそれだけです. プログラムを読んだ人に,やりたいことがそのまま伝わります. これが「名前をつけて読みやすくする」という 2 番目の目的です.

虫食い虫食い 13-A 正三角形を描く関数

正三角形を描く関数 triangle を作り,3 回呼んで並べます. ____ を埋めてください. def の行の終わりに何を打つか,中身は何をするか, そして呼ぶときに何を書くかを思い出しましょう.

fill12_triangle.py
from turtle import *

pensize(2)
pencolor("seagreen")

# 1 辺 90 の正三角形を描く関数
def triangle()____
    for i in range(____):
        fd(90)
        lt(____)

pu()
goto(-200, -40)
pd()
____

pu()
goto(-60, -40)
pd()
____

done()

13.2.4 関数を書く場所の決まり

関数を書く場所には,2 つだけ決まりがあります.

その 1:関数は,呼び出すより前(上)に書く. 次のプログラムは失敗例です.開いて実行し,エラーを見てください.

error_order.py
from turtle import *

square()        # まだ作っていない関数を呼んでいる

def square():
    for i in range(4):
        fd(80)
        lt(90)

done()

3 行目: NameError: name 'square' is not defined と出ます. 「square なんて名前は知りません」という意味です. プログラムは上から順に読まれるので,3 行目の時点では def square(): をまだ読んでいません. 関数の定義は,プログラムの上のほうにまとめて書く習慣をつけてください.

その 2:関数の中に関数を書かない. 次のように書くと,squaredraw の中だけのものになり, 外から呼ぶことができません.

やってはいけない例
def draw():
    def square():        # 関数の中に関数を書いてしまった
        for i in range(4):
            fd(80)
            lt(90)

draw()
square()                 # NameError: name 'square' is not defined

字下げが 1 段深いだけで意味がまったく変わります. def必ず左端から書く,と決めておけば安全です.

13.3 引数 — 外から値を渡す

13.3.1 大きさが違うと,関数も別々に必要?

square() は便利ですが,1 辺 80 の正方形しか描けません. 大きい正方形と小さい正方形を描きたくなったら,どうするのでしょうか. 素直にやると,こうなります.

two_sizes.py
from turtle import *

pensize(2)

def big_square():
    for i in range(4):
        fd(120)
        lt(90)

def small_square():
    for i in range(4):
        fd(40)
        lt(90)

big_square()
small_square()

done()
図 13.6 大きさが違うだけの関数が 2 つ.

中身はほとんど同じで,数が 12040 で違うだけです. 3 種類の大きさが欲しければ関数は 3 つ,10 種類なら 10 個. これでは,せっかくまとめた意味がありません.

13.3.2 引数で「大きさ」を渡す

そこで使うのが引数(ひきすう)です. 関数の名前のあとのかっこに名前を書いておくと, 呼ぶ側から値を渡せるようになります.

square_size.py
from turtle import *

pensize(2)

# 1 辺が size の正方形を描く
def square(size):
    for i in range(4):
        fd(size)
        lt(90)

square(120)
square(80)
square(40)

done()
図 13.7 ひとつの関数で 3 つの大きさの正方形が描ける.

関数はひとつだけになりました.square(120) と呼べば size の中身が 120 になり, square(40) と呼べば 40 になります. size は,関数の中だけで使える変数です (第 3 章で習った変数と同じものです).

引数つき関数の形
def 関数の名前(仮引数):
    中身の命令

関数の名前(実引数)
呼び名どこに書くか何か
仮引数(かりひきすう)def の行のかっこの中受け取った値を入れておく変数の名前.例の size
実引数(じつひきすう)呼び出しのかっこの中実際に渡す値.例の 12040
引数の数が合わないとき 呼び出しのときに実引数を書き忘れると TypeError: square() missing 1 required positional argument: 'size' と出ます.「size という引数が 1 つ足りません」という意味です. 逆に多すぎると takes 1 positional argument but 2 were given になります. 引数の数は,定義と呼び出しでぴったり合わせるのが決まりです.
虫食い虫食い 13-B 大きさを渡して星を描く

大きさを指定して星(五芒星)を描く関数を作ります. 星は 144 度ずつ 5 回曲がるのでしたね(第 2 章). ____ を埋めてください.

fill12_star.py
from turtle import *

pensize(3)
pencolor("gold")

# 1 辺が size の星を描く
def star(____):
    for i in range(5):
        fd(____)
        rt(144)

pu()
goto(-220, 0)
pd()
star(150)

pu()
goto(60, 0)
pd()
star(____)

done()

13.3.3 引数を 2 つ以上とる関数

引数はいくつでも書けます.カンマで区切るだけです. 「角の数」と「1 辺の長さ」の 2 つを渡せば, 正多角形をなんでも描ける関数になります. 第 2 章で習った「曲がる角度は 360 ÷ n」がここで効いてきます.

polygon.py
from turtle import *

pensize(2)

# 角が n 個で,1 辺が size の正多角形を描く
def polygon(n, size):
    angle = 360 / n
    for i in range(n):
        fd(size)
        lt(angle)

pu()
goto(-240, -60)
pd()
polygon(3, 100)

pu()
goto(-90, -60)
pd()
polygon(5, 70)

pu()
goto(80, -60)
pd()
polygon(8, 50)

done()
図 13.8 三角形・五角形・八角形.関数はひとつだけ.
順番を間違えるとどうなるか 引数の順番には意味があります. polygon(3, 100) は「角 3 つ・1 辺 100」ですが, polygon(100, 3) と書くと「角 100 個・1 辺 3」になり, 小さな円のような図形になってしまいます.エラーは出ません. 順番を間違えてもコンピュータは気づいてくれないので, 仮引数の名前を nsize のように分かりやすくしておきましょう.

順番を間違えたとき,エラーになることもあれば,ならないこともあります. 次はメッセージを n 回表示する関数を, 正しい順番と逆の順番で 1 回ずつ呼んだものです.

arg_order.py
# msg を n 回表示する
def repeat_msg(msg, n):
    for i in range(n):
        print(msg)

repeat_msg("こんにちは", 3)      # 正しい順番

repeat_msg(3, "こんにちは")      # 順番が逆.ここで止まる

こんにちは が 3 回表示されたあと, 3 行目: TypeError: 'str' object cannot be interpreted as an integer で止まります. 「range に文字を渡されても回数として読めません」という意味です. repeat_msg(3, "こんにちは") と書いた時点で, msg には 3 が,n には "こんにちは" が 入ってしまっているのです.

引数は名前ではなく位置で決まる 実引数は,仮引数の名前ではなく,書いた順番(位置)で対応が決まります. 1 番目に書いた値が 1 番目の仮引数へ,2 番目に書いた値が 2 番目の仮引数へ入ります. 呼ぶ側で n = 3 という変数を用意して repeat_msg(n, msg) と書いても, 名前が同じだから正しく届く,ということはありません. この渡し方を位置引数といいます.

この polygon を使うと,第 2 章で描いた「家」もすっきり書けます. 屋根は正三角形,かべは正方形.どちらも polygon ひとつで描けます.

house_func.py
from turtle import *

pensize(2)

# 正多角形を描く
def polygon(n, size):
    angle = 360 / n
    for i in range(n):
        fd(size)
        lt(angle)

# 大きさ size の家を描く
def house(size):
    polygon(4, size)      # かべ
    pu()
    lt(90)
    fd(size)
    rt(90)
    pd()
    polygon(3, size)      # 屋根
    pu()
    rt(90)
    fd(size)
    lt(90)
    pd()

pu()
goto(-240, -120)
pd()
house(120)

pu()
goto(40, -120)
pd()
house(80)

done()
図 13.9 大きさの違う家が 2 軒.関数の中で別の関数を呼んでいる.

house の中で polygon を呼んでいます. 関数の中から別の関数を呼んでよいのです. これが 3 番目の目的「部品として組み合わせる」です.13.6 節でくわしく扱います.

13.3.4 色も渡してみる

引数で渡せるのは数だけではありません. 文字(色の名前)も渡せます.型を書かなくてよい Python では, 数でも文字でも同じように書けます.

polygon_color.py
from turtle import *

pensize(4)

# 角 n 個・1 辺 size・色 col の正多角形を描く
def polygon(n, size, col):
    pencolor(col)
    angle = 360 / n
    for i in range(n):
        fd(size)
        lt(angle)

pu()
goto(-240, -60)
pd()
polygon(3, 100, "crimson")

pu()
goto(-90, -60)
pd()
polygon(5, 70, "seagreen")

pu()
goto(80, -60)
pd()
polygon(8, 50, "royalblue")

done()
図 13.10 色も引数で渡せる.
名前で分かるようにしておく 引数には「整数を渡すのか,文字列を渡すのか」を書きません. そのぶん気楽ですが, 数を渡すつもりのところに文字を渡してしまうミスを コンピュータが事前に教えてくれません. 仮引数の名前で「何を渡してほしいか」が分かるようにしておくことが, Python ではいっそう大切になります.

13.3.5 既定値つき引数 — 省略してよい引数

引数が増えてくると,毎回すべて書くのが面倒になります. =既定値(きていち)を書いておくと, その引数は省略できるようになります.

polygon_default.py
from turtle import *

pensize(3)

# size と col は省略できる
def polygon(n, size=60, col="black"):
    pencolor(col)
    angle = 360 / n
    for i in range(n):
        fd(size)
        lt(angle)

pu()
goto(-240, -40)
pd()
polygon(5)                        # size も col も既定値

pu()
goto(-100, -40)
pd()
polygon(6, 50)                    # col だけ既定値

pu()
goto(60, -40)
pd()
polygon(3, 90, "darkorange")      # 全部指定

done()
図 13.11 省略された引数には既定値が使われる.
既定値つきは後ろに 既定値つきの引数は,必ず後ろにまとめて書きます. def polygon(n=5, size): のように 既定値つきの引数を先に書くと, SyntaxError: non-default argument follows default argument というエラーになります.
虫食い虫食い 13-C らせんを描く関数

始めの長さ start から少しずつ長くしながら n 回進んで, らせんを描く関数を作ります.引数は 2 つ,色には既定値をつけます. ____ を埋めてください.

fill12_spiral.py
from turtle import *

pensize(2)
ht()

# n 回まわりながららせんを描く.色は省略できる
def spiral(n, start, col=____):
    pencolor(col)
    length = start
    for i in range(____):
        fd(length)
        lt(91)
        length = length + 3

spiral(40, 5)
spiral(28, 5, "crimson")

done()

13.3.6 引数に名前をつけて渡す — キーワード引数

13.3.3 節で見たとおり,実引数は書いた順番で仮引数に入ります. ところが Python には,順番ではなく 名前を書いて渡す方法も用意されています. 仮引数の名前=値 と書くだけです.これをキーワード引数といいます.

keyword_arg.py
# msg を n 回表示する
def repeat_msg(msg, n):
    for i in range(n):
        print(msg)

repeat_msg("位置で渡す", 2)                  # これまでどおり順番で
repeat_msg(msg="名前で渡す", n=2)            # 名前をつけて
repeat_msg(n=2, msg="順番を変えても届く")    # 名前があれば順番は自由

3 つとも同じように 2 回ずつ表示されます. 名前をつけて渡せば,順番を気にしなくてよくなるのです.

キーワード引数のうれしいところは,もうひとつあります. 既定値つきの引数(13.3.5 節)を飛ばして,後ろだけ指定できることです.

飛ばして指定する
def polygon(n, size=60, col="black"):
    ...

polygon(3, col="red")      # size は既定値の 60 のまま,色だけ赤にする

polygon(3, "red") と書いてしまうと, "red" が 2 番目の size に入ってしまいます. col="red" と名前を書けば,確実に col へ届きます. 読む人にとっても「この "red" は色のことだ」と分かる書き方です.

write の font= の正体 実は,みなさんは第 2 章からキーワード引数を使っています. write("あ", font=("Arial", 13, "normal"))font= がそれです. write には movealignfont という 既定値つきの引数があり,そのうち font だけを名前で指定していたのでした.
キーワード引数の決まり
  • キーワード引数は,位置で渡す引数より後ろに書きます.repeat_msg(n=2, "あ")SyntaxError: positional argument follows keyword argument です.
  • 名前は仮引数と同じ綴りでなければなりません.polygon(3, color="red") のように違う名前を書くと,TypeError: polygon() got an unexpected keyword argument 'color' になります.

13.4 関数の中の変数は外から見えない

13.4.1 ローカル変数

関数の中で作った変数は,その関数の中だけのものです. 関数が終わると消えてしまい,外からは見えません. これをローカル変数(局所変数)といいます.

local_error.py
def square_size():
    size = 100          # この関数の中だけの変数
    print("関数の中:", size)

square_size()
print("関数の外:", size)     # ← ここでエラーになる

関数の中: 100 まで表示されたあと, 6 行目: NameError: name 'size' is not defined で止まります. sizesquare_size の中だけの変数なので, 外では「そんな名前は知りません」と言われるのです. 仮引数も同じで,関数の外からは見えません.

読めてしまうけれど,使わない 実は Python の関数は,外側の変数を「読む」ことだけはできてしまいます. できてしまうからといって使ってはいけません. 外の変数に頼った関数は,その場所でしか動かない 「部品になれない関数」だからです. 必要な値は必ず引数で渡す.これを守ってください.

13.4.2 関数の中で外の変数に代入すると

では,関数の中から外の変数を書き換えるとどうなるでしょうか. 予想してから実行してみてください.

shadow.py
count = 0

def add_one():
    count = count + 1      # ← ここでエラーになる
    print("関数の中:", count)

add_one()
print("関数の外:", count)

4 行目: UnboundLocalError というエラーが出ます. Python は「関数の中で count に代入している」のを見て, count はこの関数のローカル変数だと判断します. ところが右辺の count + 1 を計算しようとした時点では まだ何も入っていないので,「値が入る前に使われた」と怒られるわけです.

13.4.3 どうしても外の変数を書き換えたいとき — global

関数の中から外の変数を書き換えたいときは, 関数の先頭で global 変数名 と宣言します. 「この名前は外のものを指します」という意味です.

global.py
count = 0

def add_one():
    global count           # 外の count を使うと宣言する
    count = count + 1

add_one()
add_one()
add_one()
print("呼ばれた回数:", count)

呼ばれた回数: 3 と表示されます.

global は最後の手段 global は便利ですが,できるだけ使わないでください. 外の変数を勝手に書き換える関数は,どこで何が変わったのか追いかけにくく, バグの温床になります. 「値を渡したいときは引数」「結果を返したいときは戻り値(第 14 章)」が原則で, global はどうしても仕方ないときの最後の手段です.
虫食い虫食い 13-D 変数の見える範囲

次のプログラムを完成させてください. draw_all の中から本数を数えて,外の lines に足していきます. 外の変数を書き換えるには何が必要でしょうか.

fill12_global.py
from turtle import *

ht()
lines = 0

def draw_line(length):
    ____ lines            # 外の lines を書き換える宣言
    fd(length)
    lt(60)
    lines = lines + 1

for i in range(6):
    draw_line(____)

print("引いた線の数:", ____)

done()

13.5 描き終わったら元の場所に戻す

第 2 章で,正方形の最後にもう一度 lt(90) を書く理由をこう説明しました. 「描き終わったら,始めた状態に戻す.後の回で『関数』を習うと,この約束が効いてきます」. その章が今回です.

関数を部品として使うには, その部品を使ったあとにかめがどこを向いているかが分かっていなければなりません. 次は,最後に向きを戻していない三角形の関数です.3 回呼ぶとどうなるでしょうか.

図 13.12 戻していない関数を 3 回.向きがずれていく
図 13.13 戻している関数を 3 回.きれいに重なる

左は 3 回呼ぶたびに向きが 120 度ずつずれ,別の絵になってしまいました. 右は lt(120) がもう 1 回あるだけで,何回呼んでも同じ場所に重なります. 呼ぶ前と呼んだあとで,かめの位置と向きが同じ. この性質があって初めて,関数は安心して組み合わせられる部品になります.

関数を作ったときの 4 つの確認 今日から,関数を作るときは必ずこう考えてください.
  1. その関数は,かめをどこに置いて呼ばれるのか.
  2. 描き終わったとき,かめは元の位置に戻っているか.
  3. 描き終わったとき,かめは元の向きを向いているか.
  4. ペンを上げたなら,最後に下ろしてあるか.
迷ったら,その関数を3 回続けて呼んでみるのが確実な検査です. 3 つがきれいに重なれば合格です.

13.5.1 移動する関数も部品にする

「ペンを上げて,向きを変えて,進んで,向きを戻して,ペンを下ろす」という お決まりの 5 行も,関数にしてしまいましょう. これも向きを元に戻すように作ります.

jump.py
from turtle import *

pensize(2)
ht()

# 今の向きから angle 度だけ左を向いた方向へ dist 進む(線は引かない)
# 向きは元に戻す
def jump(dist, angle):
    pu()
    lt(angle)
    fd(dist)
    rt(angle)
    pd()

def square(size):
    for i in range(4):
        fd(size)
        lt(90)

jump(240, 180)      # 左へ 240
jump(60, -90)       # 下へ 60

for i in range(4):
    square(60)
    jump(90, 0)     # 右へ 90

done()
図 13.14 jump を部品にすると,本文がとても短くなる.
書き方動き
jump(100, 0)今の向きへ 100 進む(右向きなら右へ)
jump(100, 90)今の向きの左へ 100(右向きなら上へ)
jump(100, -90)今の向きの右へ 100(右向きなら下へ)
jump(100, 180)今の向きの後ろへ 100(右向きなら左へ)
goto と jump の使い分け goto(x, y) を使う方法もありますが, goto画面の絶対的な場所を指すので, その関数は「画面のその場所でしか使えない関数」になります. jump のように今いる場所からの相対的な移動で書いておくと, どこで呼んでも同じ形が描ける,使い回しの効く部品になります.
虫食い虫食い 13-E 元に戻す関数にする

次の petal(花びら)は,ひし形を 1 つ描く関数です. 4 辺を描き終わった時点で位置は出発点に戻っていますが, 曲がった角度の合計は 60 + 120 + 60 = 240 度しかなく, 向きが戻っていません____ を埋めて,向きも元に戻る関数にしてください. 1 周は 360 度でしたね.

fill12_petal.py
from turtle import *

pensize(2)
pencolor("hotpink")
ht()

# 花びら(ひし形)を 1 枚描く
def petal(size):
    fd(size)
    lt(60)
    fd(size)
    lt(120)
    fd(size)
    lt(60)
    fd(size)
    ____(____)         # ← 向きを元に戻す

for i in range(8):
    petal(90)
    lt(45)

done()

13.6 関数を組み合わせて大きな絵にする

小さな関数を作り,それを呼ぶ関数を作り,さらにそれを呼ぶ関数を作る. こうして部品の階層を組み上げていく作り方を 構造化プログラミングといいます. 複雑さと戦うための,もっとも基本的な技法です.

13.6.1 部品を積み上げる — 円弧・レモン・花

第 2 章で「円は正 360 角形だ」と習いました. fd(2)rt(1) を繰り返すと円弧が描けます. まずこれを arc という部品にします.

arc.py
from turtle import *

pensize(2)
ht()

# 中心角 angle 度の円弧を描く
def arc(angle):
    for i in range(angle):
        fd(2)
        rt(1)

pu()
goto(-200, 60)
seth(0)          # 真右(東)を向かせる
pd()
pencolor("crimson")
arc(90)          # 90 度ぶん

pu()
goto(0, 60)
seth(0)
pd()
pencolor("seagreen")
arc(180)         # 半円

pu()
goto(180, 60)
seth(0)
pd()
pencolor("royalblue")
arc(360)         # ぐるりと 1 周=円

done()
図 13.15 arc(90)arc(180)arc(360)
seth(0) で向きを揃える seth(0) は「かめを真右(東)向きにする」命令です(set heading の略). goto は場所を変えるだけで向きは変えませんarc は描き終わったあと向きが変わったままなので, 次の円弧を同じ向きから描き始めたいときは seth(0) で向きを揃えます. この面倒さこそ,13.5 節の「元の向きに戻す」約束が大事な理由です.

次に,この arc使って レモン(木の葉)の形を描く関数を作ります. 120 度の円弧を 2 つ向かい合わせるだけです. 最後に向きを戻すのを忘れないでください.

lemon.py
from turtle import *

pensize(2)
pencolor("darkorange")
ht()

def arc(angle):
    for i in range(angle):
        fd(2)
        rt(1)

# レモン(木の葉)の形を描く
def lemon():
    arc(120)
    rt(60)
    arc(120)
    rt(60)        # 向きを元に戻す

lemon()

done()
図 13.16 レモン型.arc を 2 回呼んでいるだけ.

さらに,この lemon使って花を描きます. lemon が「位置も向きも元に戻す」部品になっているので, 少しずつ回しながら呼ぶだけで花びらが並びます.

flower.py
from turtle import *

pensize(2)
ht()

def arc(angle):
    for i in range(angle):
        fd(2)
        rt(1)

def lemon():
    arc(120)
    rt(60)
    arc(120)
    rt(60)

# 花びら n 枚の花を描く
def flower(n, col):
    pencolor(col)
    for i in range(n):
        lemon()
        lt(360 / n)

flower(6, "hotpink")

done()
図 13.17 6 枚の花びら.本文は flower(6, "hotpink") の 1 行.

flowerlemon を呼び, lemonarc を呼び, arcfdrt を呼んでいます. 3 段の積み木です.いちばん上の flower(6, "hotpink") だけを読めば, 「ピンクの花を 6 枚の花びらで描く」ことがすぐ分かります.

13.6.2 関数構成図を先に描く

関数が増えてくると,呼び出し関係がこんがらがります. プログラムを書き始める前に, どんな関数を作り,どれがどれを呼ぶかを紙に書いておきましょう. これを関数構成図といいます.

たとえば「窓のついた家」を描くなら,こんな構成になります.

  • house(size) 家を描く
    • wall(size) かべを描く
      • polygon(n, size) 正多角形を描く
    • roof(size) 屋根を描く
      • jump(dist, angle) 線を引かずに移動する
      • polygon(n, size) 正多角形を描く
    • windows(size) 窓を並べる
      • jump(dist, angle) 線を引かずに移動する
      • polygon(n, size) 正多角形を描く

この図をそのままプログラムにしたものが次です. 上から下へ,大きい部品から小さい部品の順に並べてあります.

house_structure.py
from turtle import *

pensize(2)
ht()

# --- いちばん小さい部品 ---

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

# 正多角形を描く
def polygon(n, size):
    angle = 360 / n
    for i in range(n):
        fd(size)
        lt(angle)

# --- 家の部品 ---

def wall(size):
    polygon(4, size)

def roof(size):
    jump(size, 90)
    polygon(3, size)
    jump(size, -90)

def windows(size):
    w = size / 5
    jump(w, 0)
    jump(w * 2, 90)
    polygon(4, w)
    jump(w * 2.5, 0)
    polygon(4, w)
    jump(w * 3.5, 180)
    jump(w * 2, -90)

# --- 家ぜんぶ ---

def house(size):
    wall(size)
    roof(size)
    windows(size)

# --- 本文 ---

pencolor("saddlebrown")
jump(240, 180)
jump(120, -90)
house(130)

pencolor("teal")
jump(200, 0)
house(90)

done()
図 13.18 窓つきの家が 2 軒.関数は 6 つ,どれも 10 行以内.
1 関数 10 行以内 1 つの関数は 10 行以内に収めてください. 長くなったら,それは「2 つのことをやっている」合図です. 中身を 2 つに割って,それぞれに名前をつけましょう. 名前が思いつかないときは,まだ自分でも何をしているか整理できていない証拠です.

13.6.3 実は fd も rt も関数だった

ここまで来ると,種明かしができます. 第 2 章から使ってきた fdrtpencolor も, 実は誰かが def で書いた関数です. from turtle import * は, その関数の入った箱を開けて使えるようにする命令でした.

turtle の中では,だいたいこんなふうに書かれている
def fd(distance):
    ...(かめを distance だけ前に進める中身)...

def rt(angle):
    ...(かめを angle 度だけ右に回す中身)...

ある目的のために作られた関数の集まりをライブラリといいます. turtle は「タートルグラフィックスのライブラリ」で, 第 14 章で使う math は「数学の計算のライブラリ」です. みなさんが今日作った polygonarc も, 立派な「自分のライブラリ」の第一歩です.

13.6.4 もとからある関数 — 組み込み関数

fdrt は,from turtle import * と 書いて初めて使えるようになる関数でした. ところが,何も import しなくても最初から使える関数もあります. これを組み込み関数(くみこみかんすう)といいます.

実は,みなさんは気づかないうちに,ずっと組み込み関数を使ってきました. printinput(第 9 章),int(第 9 章), range(第 6 章). どれも名前のうしろにかっこを書いて呼ぶ形をしています. そう,これらはすべて関数だったのです.

builtin.py
# どれも import せずに使える組み込み関数
print(len("こんにちは"))         # 文字数 → 5
print(max(3, 8, 5))              # いちばん大きい数 → 8
print(min(3, 8, 5))              # いちばん小さい数 → 3
print(abs(-7))                   # 絶対値 → 7
print(round(3.14159, 2))         # 四捨五入 → 3.14
print(int("42") + 1)             # 文字を数に変える → 43
print(str(2026) + "年")          # 数を文字に変える → 2026年
書き方何をするか
print(x)画面に表示する.表示するだけで,答えは持ち帰らない
input()キーボードから 1 行読む(第 9 章)
int(s)float(s)文字を整数・小数に変える(第 9 章)
str(x)数を文字に変える
len(s)文字数を数える
max(…)min(…)いちばん大きい数・小さい数
abs(x)絶対値(符号を取った大きさ)
round(x, n)小数第 n 位で四捨五入する
range(n)0 から n-1 までを順に取り出す(第 6 章)
type(x)その値の種類を調べる
関数は 3 種類ある 関数は,どこから来たかで 3 つに分けられます.
  1. 組み込み関数.printlenint など.何も書かずに,いきなり使える.
  2. ライブラリの関数.fdrt など.from turtle import * のように import してから使う.
  3. 自分で作った関数.polygonarc など.def で自分が書いたもの.
3 つの違いはどこに書いてあるかだけで, 使い方は「名前を書いてかっこをつけ,引数を渡す」でまったく同じです. len("あいう")polygon(5, 70) も, 書き方の形は少しも変わりません.

表をよく見ると,print だけが少し違います. len("こんにちは")5 という答えを持ち帰ってくるので, print(len("こんにちは")) のように,そのまま表示できました. print のほうは画面に出すだけで,何も持ち帰りません. この「持ち帰る・持ち帰らない」の違いが,次の第 14 章の主役です.

名前を奪わない 組み込み関数と同じ名前を,自分の関数名や変数名に使わないでください. len = 5 と書いた瞬間に,len は 5 という数になり, 本来の len("あいう")TypeError: 'int' object is not callable で動かなくなります. 13.7 節の 4 番目の決まりは,このためのものです.
本領を発揮するのは第 16 章 lenmaxmin は, ここでは文字や 2〜3 個の数にしか使えず,ありがたみが薄いかもしれません. 第 16 章でリストを習うと, 「100 個の数の中でいちばん大きいもの」を max の 1 行で取り出せるようになり, これらの関数が一気に主役になります.

13.7 関数の名前の付け方

関数の値打ちは,半分は名前で決まります. 中身が正しくても,名前が afunc1 では, 読む人(=来週の自分)に何も伝わりません.

まず,名前に使える文字の決まりから確認します. これは第 3 章で習った変数名の決まりとまったく同じです. 英数字と _ が使えて,数字では始められず, forifdef のような Python があらかじめ使っていることばは名前にできません. その上で,次の 4 つを守ってください.

  1. 動詞で始める.関数は「何かをする」ものだからです.draw_house(家を描く),move_to(〜へ移動する),make_color(色を作る).ただし squarelemon のように「描くもの」がはっきりしている図形は,名詞だけでも通じます.
  2. 小文字と _ でつなぐ.drawhouse より draw_house のほうが読めます.この書き方をスネークケースといい,Python の標準です.
  3. 中身を言い当てる.draw だけでは何を描くのか不明です.draw_star のように具体的に書きます.
  4. すでにある名前を避ける.printfdlist などを関数名にすると,元の命令が使えなくなります.
よくない名前よい名前理由
a()draw_star()何をするのか分からない
kakukakei(n, s)polygon(n, size)ローマ字書きは通じにくい.英語で短く
drawtwobighouses()house(size)「2 つ」「大きい」は引数と呼ぶ回数で決めること
Square()square()大文字始まりはクラスに使う(Python の習慣)
print(x)show_result(x)元からある命令の名前を奪わない
引数の名前も大事 仮引数の名前も同じです.def polygon(n, size) と書いてあれば, 呼ぶ側は「角の数と大きさを渡すのだな」と分かります. def polygon(x, y) だと,座標を渡すように見えてしまいます.

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

書き方意味
def 名前():引数なしの関数を定義する.次の行から字下げして中身を書く
名前()関数を呼び出す.ここで中身が実行される
def 名前(a, b):引数を 2 つとる関数を定義する(ab が仮引数)
名前(10, 20)引数を渡して呼び出す(1020 が実引数)
def 名前(a, b=5):b に既定値 5.名前(10) と省略して呼べる
名前(10, b=7)キーワード引数.名前を書いて渡すと,順番を気にしなくてよい
len(s)max(a, b)組み込み関数.importdef も要らず,最初から使える
global x関数の中で,外の変数 x を書き換えると宣言する
# コメント関数の上に「何をする関数か」を 1 行書く習慣をつける
今回よくある間違い
症状原因と直し方
SyntaxError: expected ':'def の行の終わりにコロンが無い
IndentationError: expected an indented blockdef の次の行を字下げしていない
NameError: name 'xxx' is not defined呼び出しより下に def を書いている/綴り違い/関数の中で作った変数を外で使っている
TypeError: xxx() missing 1 required positional argument実引数が足りない.定義のかっこと呼び出しのかっこを見比べる
TypeError: xxx() takes 1 positional argument but 2 were given実引数が多すぎる
TypeError: 'str' object cannot be interpreted as an integer数を渡すはずのところに文字を渡している.引数の順番が逆になっていないか確かめる
SyntaxError: positional argument follows keyword argument名前=値 で渡した引数より後ろに,ふつうの引数を書いている
TypeError: xxx() got an unexpected keyword argument名前=値 の名前の綴りが,仮引数と違っている
UnboundLocalError関数の中で外の変数に代入している.global を書くか,引数で渡す
エラーは出ないが何も描かれないdef しただけで呼んでいない/呼び出しのかっこを忘れている
2 回目以降がずれる関数が元の位置・向きに戻していない.13.5 節へ

演習

以下の演習には出発点となるプログラムが入っています. 「▶ この演習を開く」で右側に読み込み,それを発展させて作品にしてください. どの演習も,作った関数を 2 回以上呼ぶことを条件にします. 1 回しか呼ばないなら,関数にした意味が半分しかありません.

発展のヒント 発展のしかたに迷ったら,次のどれかを試してください.
  • 引数を増やす(大きさ・色・角の数・回数)
  • その関数を呼ぶ,もう 1 つ上の関数を作る
  • for と組み合わせて,たくさん並べる・回しながら重ねる
  • 第 7 章で習った入れ子の繰り返しで,縦横に並べる
演習演習 13-1 円を描く関数

円を描く関数 circle_by_hand() を作り,2 回以上呼ぶプログラムです. まずそのまま実行して,動作を確かめてください. そのうえで,大きさを引数で渡せるように直し, 大小の円を並べた模様に発展させてください.

ヒント 大きさを変えるには fd(1)1 を引数にします. def circle_by_hand(step): として fd(step) と書けば, step が大きいほど大きな円になります. ただし大きくしすぎると画面からはみ出します.step は 2 くらいまでにしましょう.
演習演習 13-2 大きさが変わる家

大きさを指定して家を描く関数 house(size) があります(窓は無くてかまいません). 2 回以上呼んで動くことを確かめたうえで,発展させてください.

  • 色も引数で渡せるようにする
  • for で,だんだん小さくなる家を並べる
  • 窓・ドア・煙突を描く関数を足して,house から呼ぶ
出発点の 2 軒の家
演習演習 13-3 中心角を指定して円弧を描く

中心角を指定して円弧を描く関数 arc(angle) があります. arc(360) なら円,arc(180) なら半円, arc(1) なら点のようになります. いろいろな中心角で呼んで,形を確かめてください. そのうえで,半径も引数で渡せるように直し, 円弧を組み合わせた模様に発展させてください.

arc(120)
arc(300)
演習演習 13-4 レモンと蝶 — 部品を組み合わせる

arc を部品に使ってレモン型を描く lemon() と, さらにその lemon を部品に使って蝶を描く butterfly() があります. どの関数がどの関数を呼んでいるかを確かめてから,発展させてください.

  • レモンの大きさを引数で渡せるようにする
  • 羽の角度を変えて,別の生き物にしてみる
  • 触角や体を描く関数を足して butterfly から呼ぶ
演習演習 13-5 花 — 関数を再利用する

lemon() をそのまま再利用して花を描く flower(n, col) があります. lemon は一文字も書き換えていないことに注目してください. 「元の位置・元の向きに戻す」部品にしてあるから,こうして使い回せるのです. 花びらの数・色・大きさを変えて,花畑に発展させてください.

演習演習 13-6 マス目

row 行 col 列のマス目を描く関数 grid(row, col, size) を完成させてください. 出発点のプログラムには square(size) が用意してあります. 第 7 章で習った繰り返しの入れ子を使います.

  • grid(2, 3, 50) で 2 行 3 列のマス目になること
  • grid(5, 7, 20) でも正しく描けること
  • この 2 つを両方呼んで,位置をずらして並べること
ヒント r 行目・c 列目のマスの左下は, 原点から見て右へ c * size,下へ r * size の場所です. goto(x0 + c * size, y0 - r * size) で移動してから square(size) を呼べば並びます.
自由演習 13-7 構造化プログラミングで作品をつくる

最後は自由制作です.ただし,ひとつだけ厳しいルールがあります.

  • 1 つの関数は 10 行以内.本文(関数の外)も 10 行以内にすること
  • 関数を3 つ以上作ること
  • そのうち少なくとも 1 つは,別の関数を呼ぶ関数にすること
  • すべての関数に,何をする関数かを 1 行コメントで書くこと
  • 図形を描く関数は,元の位置・元の向きに戻すこと
ヒント いきなり書き始めず,まず紙に関数構成図を描いてください (13.6.2 節).「何を描きたいか」→「そのために何と何が必要か」→ 「そのために何が必要か」と,上から分けていくのがコツです. 出発点のプログラムは,星・十字・歯車を描く 3 つの部品からできています. 読んで,仕組みを理解してから作り変えてください.

まとめ

  • 同じものを 2 回以上書いたら関数にする.まとめる・名前をつける・部品として組み合わせる,の 3 つが目的.
  • def 名前(): で定義し,名前() で呼び出す.定義しただけでは何も起きない
  • 呼ぶと関数の中へ飛び,中身を実行し,呼んだ場所の次の行へ戻る
  • 関数は呼び出しより前(上)に書く.関数の中に関数を書かない.
  • 引数で外から値を渡す.定義側が仮引数,呼び出し側が実引数.引数は何個でもよく,= で既定値をつければ省略できる.Python では型を書かない.
  • 引数は書いた順番(位置)で仮引数に入る.名前=値 と書くキーワード引数なら順番は自由になり,既定値つきの引数を飛ばして指定できる.
  • printlenint などは最初から使える組み込み関数fdimport して使うライブラリの関数.自分で def した関数も含め,使い方はどれも同じ.
  • 関数の中で作った変数はローカル変数で,外からは見えない.外の変数を書き換えるには global が要るが,できれば引数と戻り値で済ませる.
  • 描き終わったら元の位置・元の向きに戻す.これを守った関数だけが,組み合わせられる部品になる.
  • 1 つの関数は 10 行以内.長くなったら分ける.

今回の関数は「やってもらう」だけで,結果を受け取ることができませんでした. 次の回では return を習います. 計算した答えを関数から持ち帰らせて, 「入力(引数)と出力(戻り値)のある関数」を作れるようになります.