4 関数

関数は、プログラム内のミニプログラムのようなものです。 Pythonには、前章までに紹介したprint()関数、input()関数、len() 関数のような組み込み関数がありますが、自分で関数を作ることもできます。本章では、関数を作成し、ブログラム内で関数の実行順を決める呼び出しスタックを調べ、関数内外での変数のスコープについて説明します。

関数の作成

関数の働きを理解するために一つ作ってみましょう。次のコードをhelloFunc.pyという名前で保存してください。

def hello():
    # 3つのあいさつを表示
    print('Good morning!')
    print('Good afternoon!')
    print('Good evening!')

hello()
hello()
print('ONE MORE TIME!')
hello()

1行目はdef文です。hello()という名前の関数を定義しています。def文に続くブロックのコードは関数の本体です。関数が呼び出されたときにこのコードが実行されます。関数の定義時に実行されるのではありません。

関数を定義したあとのhello()の行が関数の呼び出しです。関数名のあとにかっこをつけて関数を呼び出します。かっこ内に引数があることもあります。プログラム実行が関数呼び出しにたどり着くと、関数本体の最初の行からコードを実行し始めます。関数本体の最後まで実行すると、関数が呼び出された行に戻り、そこからコードの実行を続けます。

このプログラムでは hello()を3回呼び出していますから、hello()関数が3回実行されます。このプログラムを実行すると、次のように出力されます。

Good morning!
Good afternoon!
Good evening!
Good morning!
Good afternoon!
Good evening!
ONE MORE TIME!
Good morning!
Good afternoon!
Good evening!

関数を作る主な目的は、何回も実行されるコードをまとめることです。関数を作らなければ、次のようにその都度コピーアンドペーストすることになります。

print('Good morning!')
print('Good afternoon!')
print('Good evening!')
print('Good morning!')
print('Good afternoon!')
print('Good evening!')
print('ONE MORE TIME!')
print('Good morning!')
print('Good afternoon!')
print(' Good evening!')

一般に、コードをコピーアンドペーストすることは避けたほうがよいです。バグを発見したなどの理由でコードを更新するときに、コピーしたコードを全部更新するのは大変だからです。

プログラミングの経験を積むと、重複排除コードを書くようになります。コードのコピーアンドペーストをしないということです。重複を避けると、プログラムが短くなり、読みやすくて更新しやすくなります。

引数とパラメータ(仮引数)

print()関数やlen()関数を呼び出すときに、かっこ内に引数と呼ばれる値を渡すことがあります。それと同じように引数を取る関数を定義できます。次のコードをhelloFunc2.pyという名前で保存してください。

❶ def say_hello_to(name):
    # 与えられた名前に対して3つのあいさつを表示
  ❷ print('Good morning, ' + name)
    print('Good afternoon, ' + name)
    print('Good evening, ' + name)

❸ say_hello_to('Alice')
say_hello_to('Bob')

このプログラムを実行すると、次のように出力されます。

Good morning, Alice
Good afternoon, Alice
Good evening, Alice
Good morning, Bob
Good afternoon, Bob
Good evening, Bob

このプログラムでのsay_hello_to()関数の定義には、nameというパラメータ(仮引数)があります(➊)。パラメータとは、引数(真引数)を受け取る変数のことです。引数を渡して関数を呼び出したときに、パラメータが引数を受け取ります。say_hello_to() 関数の1回目の呼び出しでは、'Alice'という引数が渡されています(➌)。プログラム実行は関数を実行し、パラメータnameに'Alice'という引数が設定されます。そして、その引数とあいさつがprint()文で表示されます(➋)。関数呼び出しに渡した値に応じて微妙に異なる命令を実行したい場合には、関数内でパラメータを使います

パラメータに格納される値は関数の呼び出しが終わると失われます。例えば、先のプログラムでsay_hello_to('Bob') のあとにprint(name)を書き加えても、エラーになります。変数nameに値が入っていないからです。say_hello_to('Bob')の呼び出し後にパラメータの変数は破棄されるので、print(name)に使われている変数nameは存在しないのです。

定義する、呼び出す、渡す、引数、パラメータという用語は混乱しがちです。これらの用語をおさらいするためにコード例を見てみましょう。

❶ def say_hello_to(name):
    # 与えられた名前に対して3つのあいさつを表示
    print('Good morning, ' + name)
    print('Good afternoon, ' + name)
    print('Good evening, ' + name)
❷ say_hello_to('Al')

関数を定義するということは関数を作ることです。 spam = 42 のような文で変数spamを作るのと同じです。def文でsay_hello_to()関数を定義しています(➊)。say_hello_to('Al')の行は、先ほど作った関数を呼び出し、 関数のコードを実行します(➋)。この関数呼び出しでは、'Al'という文字列値を関数に渡しています。呼び出し時に関数に渡される値は引数です。引数'Al'がローカル変数に割り当てられます。引数'Al'が割り当てられる変数はパラメータです。

これらの用語を混同しがちですが、ここで正しく理解しておくと、この章の内容を正確に把握できるようになります。

返り値とreturn文

'Hello'のような引数を渡してlen()関数を呼び出すと、その文字数である5のような整数に評価されます。一般化して言うと、関数呼び出しにより評価される値を、関数の返り値(戻り値)と呼びます。

def文により自分で関数を作るときに、return文で返り値を指定できます。次のような形になります。

  • returnというキーワード
  • 関数が返す値または式

式を使うと、その式が評価する値が返り値になります。例えば、次のプログラムは、渡された引数の数字によって別々の文字列を返す関数を定義しています。次のコードをmagic8Ball.pyという名前で保存してください。

❶ import random

❷ def get_answer(answer_number):
    # answer_numberの値1から9に応じて答えを返す
  ❸ if answer_number == 1:
        return 'It is certain'
    elif answer_number == 2:
        return 'It is decidedly so'
    elif answer_number == 3:
        return 'Yes'
    elif answer_number == 4:
        return 'Reply hazy try again'
    elif answer_number == 5:
        return 'Ask again later'
    elif answer_number == 6:
        return 'Concentrate and ask again'
    elif answer_number == 7:
        return 'My reply is no'
    elif answer_number == 8:
        return 'Outlook not so good'
    elif answer_number == 9:
        return 'Very doubtful'

print('Ask a yes or no question:')
input('>')
❹ r = random.randint(1, 9)
❺ fortune = get_answer(r)
❻ print(fortune)

最初にrandomモジュールをインポートしています(➊)。次にget_answer()関数を定義しています(➋)。関数の呼び出しではなく定義ですから、この時点でコードが実行されるわけではありません。次に、1と9という2つの引数を渡してrandom.randint()関数を呼び出します(➍)。1から9までの整数(1と9を含む)に評価され、変数rに格納されます。

rを引数としてget_answer()関数を呼び出します(➎)。プログラム実行はこの関数の最初に移動し、rの値がパラメータanswer_numberに格納されます(➌)。この関数は、answer_numberの値に応じて文字列を返します。プログラム実行はget_answer()を呼び出した行に戻ります(➎)。変数fortuneに返り値が代入され、print()の呼び出しに渡され、画面に表示されます(➏)。

返り値を別の関数呼び出しの引数として渡すことができますから、次の3行を縮めて書くことができます。

r = random.randint(1, 9)
fortune = get_answer(r)
print(fortune)

この3行は次の1行と同じです。

print(get_answer(random.randint(1, 9)))

式は値と演算子から成り立っていることを思い出してください。関数呼び出しは返り値に評価されますから、式中で使うことができます。

None値

PythonにはNone値という値が存在します。値がないことを表します。None値はNoneTypeデータ型の唯一の値です。(値がないことを表す値として、ほかのプログラミング言語では、null、nil、undefinedという値があったりします。)ブール値のTrueとFalseと同じように、Noneは大文字のNで始めます。

値がないことを表すこの値は、変数に何らかの値を格納しなければならないのだけれども実際的な値を格納したくないときに便利です。print()の返り値はNoneです。print()関数は画面にテキストを表示しますが、len()関数やinput()関数のように返り値は必要ありません。しかし、関数はすべて値を返さなければならないので、print()関数はNoneを返します。これを確認するために、次のコードを対話型シェルに入力してください。

>>> spam = print('Hello!')
Hello!
>>> None == spam
True

Pythonは、明示的なreturn文がなくても、関数の定義の最後にreturn Noneを付け加えます。whileやforのループで最後に到達するとcontinue文が書かれていなくても最初に戻るのと似ています。また、return文を値なしで使うと(returnというキーワードだけを書くと)、Noneが返されます。

名前付きパラメータ

Pythonの関数呼び出しでは、引数を位置によって特定することが多いです。例えば、random.randint(1, 10)はrandom.randint(10, 1)とは異なります。前者は1から10までの整数をランダムに返します。第一引数が範囲の始まりで第二引数が範囲の終わりです。後者はエラーになります。

位置引数とは異なり、名前付きパラメータは、関数の呼び出し時の引数の前の名前によって特定されます。名前付きパラメータは、キーワードパラメータやキーワード引数と呼ばれることもありますが、Pythonのキーワードとは関係ありません。名前付きパラメータはオプションの引数として使われることが多いです。例えば、print()関数にはendとsepというオプションのパラメータがあります。それぞれ、出力の終わりをどうするか、引数の間(区切り)をどうするかを指定するパラメータです。次のコードを実行するとします。

print('Hello')
print('World')

出力は次のようになります。

Hello
World

print()関数は自動的に改行文字を追加するので、2つの出力が別々の行に現れます。end名前付きパラメータを設定すると、改行文字から変更できます。例えば、次のコードを実行するとします。

print('Hello', end='')
print('World')

出力は次のようになります。

HelloWorld

'Hello'のあとに改行がなくなるので、一行で出力されます。改行文字の代わりに空文字列が出力されています。print()関数を呼び出すたびに付け加えられる改行を無効にできます。コイン投げの結果を表示したいとします。 coinflip.pyプログラムのように、出力を1行で表示したほうが見やすいです。

import random
for i in range(100):  # コイン投げを100回実行
    if random.randint(0, 1) == 0:
        print('H', end=' ')
    else:
        print('T', end=' ')
print()  # 最後に1つ改行

このプログラムを実行すると、H(head、表)とT(Tail、裏)の結果が1行でコンパクトに表示されます。1行に1回の結果を表示するよりも見やすいです。

T H T T T H H T T T T H H H H T H H T T T T T H T T T T T H T T T T T H T H T
H H H T T H T T T T H T H H H T H H T H T T T T T H T T H T T T T H T H H H T
T T T H T T T T H H H T H T H H H H T H H T

また、print()関数に複数の文字列を渡すと、自動的に半角スペースで区切られます。それを確かめるために、以下の式を対話型シェルに入力してみてください。

>>> print('cats', 'dogs', 'mice')
cats dogs mice

sep名前付きパラメータに区切り文字を渡すと、そのデフォルトの挙動を変更できます。以下の式を対話型シェルに入力してみてください。

>>> print('cats', 'dogs', 'mice', sep=',')
cats,dogs,mice

自分で作る関数にキーワード引数を設けることもできます。しかしその前に第6章と第7章でリストと辞書のデータ型を学ぶ必要があります。今のところは、関数の呼び出し時にオプションのキーワード引数を渡せる関数があるということを知っておいてください。

呼び出しスタック

誰かととりとめのない会話をしている場面を想像してください。友人のAliceの話をしたら同僚のBobのことを思い出しましたが、その前にいとこのCarolのことを説明しなければなりません。Carolの説明をしてBobの話に戻り、Bobの話をしてからAliceの話に戻ります。しかしその前に兄のDavidことを思い出したのでその話をしてから、Aliceの話に戻ります。この会話は図4-1のようなスタック構造をしています。スタックでは、項目(先ほどの会話の例で言うと話題)を一番上に追加するか一番上から削除するかのどちらかしかできません。現在の項目(話題)が常にスタックの一番上にあります。

図 4-1:とりとめのない会話のスタック

こうしたとりとめのない会話と同じように、関数呼び出しはプログラム実行を関数の最初に移動させてから元の呼び出し場所に戻って来ます。Pythonは関数が呼び出された行を覚えていて、return文に到達するとその行に戻ります。最初に実行した関数内で別の関数を呼び出すと、プログラム実行は別の関数を呼び出してから、最初の関数の実行に戻ります。スタックの一番上にある関数呼び出しがプログラム実行の現在位置です。

次のコードをabcdCallStack.pyという名前で保存してください。

def a():
    print('a() starts')
  ❶ b()
  ❷ d()
    print('a() returns')

def b():
    print('b() starts')
  ❸ c()
    print('b() returns')

def c():
  ❹ print('c() starts')
    print('c() returns')

def d():
    print('d() starts')
    print('d() returns')

❺ a()

この関数を実行すると、出力は次のようになります。

a() starts
b() starts
c() starts
c() returns
b() returns
d() starts
d() returns
a() returns

a()が呼び出されると(➎)、b()を呼び出し(➊)、続いてc()を呼び出します(➌)。c()はほかの関数を呼び出さず、c() startsとc() returnsを表示します(➍)。そして、c()を呼び出したb()の行に戻ります(➌)。c()を呼び出したb()の行に戻り、b()の実行を終えると、b()を呼び出したa()の行に戻ります(➊)。b()を呼び出した次の行、つまりd()を呼び出す行に進みます(➋)。c()と同様d()もほかの関数を呼び出しません。d() startsとd() returnsを表示し、d()を呼び出したa()の行に戻ります(❷)。a()の最終行でa() returnsを表示し、a()を呼び出した元の行に戻ります(➎)。

呼び出しスタックは、Pythonが関数を呼び出したあとに戻る場所を覚えておく仕組みです。呼び出しスタックはプログラム中の変数に保存されているのではなくコンピュータのメモリに保存されていて、Pythonが背後で処理しています。プログラムが関数を呼び出すと、Pythonは呼び出しスタックの一番上にフレームオブジェクトを作成します。フレームオブジェクトが関数を呼び出した行番号を保存することにより、Pythonが関数の実行後に戻る場所がわかります。さらに関数を呼び出すと、呼び出しスタックの一番上に別のフレームオブジェクトを作成します。

関数の呼び出しが終わると、Pythonはスタックの一番上のフレームオブジェクトを削除し、そのフレームオブジェクトに保存されている行番号に戻ります。フレームオブジェクトは必ず一番上から処理されます。図4-2はabcdCallStack.pyで関数の実行ごとに呼び出しスタックがどうなっているかを示しています。

図 4-2:abcdCallStack.pyにおける関数の実行ごとの呼び出しスタックのフレームオブジェクト

呼び出しスタックの一番上には現在実行されている関数があります。呼び出しスタックが空になると、プログラム実行は関数外にあります。

プログラムを書くのに呼び出しスタックの技術的な細部まで知る必要はありません。関数を呼び出したあとには呼び出し行に戻ることを理解していれば十分です。呼び出しスタックを理解すると、次のセクションで説明するローカルスコープとグローバルスコープを理解する助けになります。

ローカルスコープとグローバルスコープ

呼び出された関数内のコードだけが、その関数内のパラメータと変数にアクセスできます。関数内の変数は、その関数のローカルスコープの中にあります。逆に、すべての関数の外にある変数には、プログラムのどこからでもアクセスできます。関数外の変数は、グローバルスコープの中にあります。ローカルスコープの中にある変数をローカル変数と呼び、グローバルスコープの中にある変数をグローバル変数と呼びます。変数は必ずローカル変数かグローバル変数のどちらかになります。

スコープは変数を入れている容器だと考えてください。グローバルスコープは、プログラムの実行開始時に作られ、一つだけ存在します。プログラムが終了すると、グローバルスコープは破棄され、その中に入っている変数もすべて破棄されます。ローカルスコープは関数の呼び出し時に新しく作られます。関数内の変数はすべてその関数のローカルスコープ内にあります。関数の実行が終わると、ローカルスコープは破棄され、その中にある変数も破棄されます。

Pythonでは、関数がパラメータと返り値を通じてのみ変数を変更できるようにするために、スコープが存在しています。そうすることでバグの原因となる可能性のあるコードの行数を絞り込めます。もしグローバル変数しかなかったとすると、変数におかしな値が設定されているせいでバグが発生したら、どこでその値が設定されたかを追いかけるのが大変です。プログラムのどこからでも変数に値を設定できることになるからです。得てしてプログラムは数千行に及びます。これに対し、ローカル変数におかしな値が設定されているせいでバグが発生したら、その1つの関数を追うだけですみます。

そのため、小さなプログラムでグローバル変数を使っても構いませんが、大きなプログラムでグローバル変数に頼るのは悪い習慣です。

スコープのルール

ローカル変数とグローバル変数を使うときには、以下のルールを意識してください。

  • すべての関数の外側であるグローバルスコープでローカル変数は使用不可
  • 関数内のローカルスコープで別のローカルスコープ内の変数は使用不可
  • ローカルスコープでグローバル変数は使用可
  • スコープが異なれば同じ名前を別々の変数に使える(ローカル変数spamとグローバル変数spamは別物)

これらのルールを例で確かめてみましょう。

グローバルスコープでローカル変数は使用不可

次のコードを実行するとエラーが発生します。

def spam():
  ❶ eggs = 'sss'
spam()
print(eggs)

このプログラムの出力は次のようになります。

Traceback (most recent call last):
  File "C:/test1.py", line 4, in <module>
    print(eggs)
NameError: name 'eggs' is not defined

変数eggsはspam()が呼び出されて変数が作成されたローカルスコープ内に存在します(➊)。spam()の実行後にそのローカルスコープは破棄され、変数eggsは存在しなくなります。よって、print(eggs)を実行しようとすると、eggsが定義されていないというエラーが発生します。プログラム実行がグローバルスコープにあるときは、ローカルスコープは存在しませんから、ローカル変数もありません。グローバルスコープではグローバル変数しか使えないのです。

ローカルスコープでほかのローカルスコープの変数は使用不可

関数が呼び出されるたびに新しいローカルスコープが作られます。ある関数内で別の関数を呼び出すときもそうです。次のプログラムを見てください。

def spam():
    eggs = 'SPAMSPAM'
  ❶ bacon()
  ❷ print(eggs)  # 'SPAMSPAM'が表示される

def bacon():
    ham = 'hamham'
    eggs = 'BACONBACON'

❸ spam()

プログラムの実行を開始すると、spam()関数が呼び出され(❸)、ローカルスコープが作られます。spam()関数が、ローカル変数eggsに 'SPAMSPAM'を設定し、bacon()関数を呼び出して(➊)、2つ目のローカルスコープを作成します。同時に複数のローカルスコープが存在します。この新しいローカルスコープ内で、ローカル変数hamに 'hamham'が設定され、ローカル変数eggsに 'BACONBACON'が設定されます。これはspam()のローカル変数eggsとは別物です。この時点で、プログラムには、2つのeggsという名前のローカル変数が同時に存在します。1つはspam()のローカル変数で、もう1つはbacon()のローカル変数です。

bacon()の実行が終わると、そのローカルスコープは破棄され、そのローカル変数eggsも破棄されます。spam()関数のプログラム実行が継続し、eggsの値を表示します(❷)。spam()のローカルスコープはまだ存在しており、変数eggsはspam()関数のローカルスコープにだけあります。その値は'SPAMSPAM'です。プログラムはこれを表示します。

ローカルスコープでグローバル変数は使用可

ここまで、グローバルスコープでローカル変数は使用不可であることと、ローカルスコープでほかのローカルスコープの変数は使用不可であることを示してきました。次のプログラムを見てください。

def spam():
    print(eggs)  # 'GLOBALGLOBAL'が表示される
eggs = 'GLOBALGLOBAL'
spam()
print(eggs)

eggsという名前のパラメータも、変数eggsに値を代入しているコードも、spam()関数にはありませんから、spam()でeggsが使われると、グローバル変数のeggsだとPythonは解釈します。よって、このプログラムを実行すると、'GLOBALGLOBAL'が表示されます。

同名のローカル変数とグローバル変数は別物

同名のグローバル変数とローカル変数を別物として使うことがPythonでは技術的に可能です。技術的に可能だからといってそのようなことはしないでください。同名のグローバル変数とローカル変数を使うとどうなるかを確認するために、次のコードをlocalGlobalSameName.pyという名前で保存してください。

def spam():
  ❶ eggs = 'spam local'
    print(eggs)  # 'spam local'が表示される

def bacon():
  ❷ eggs = 'bacon local'
    print(eggs)  # 'bacon local'が表示される
    spam()
    print(eggs)  # 'bacon local'が表示される

❸ eggs = 'global'
bacon()
print(eggs)  # 'global'が表示される

このプログラムを実行すると、次のように出力されます。

bacon local
spam local
bacon local
global

このプログラムには3つの異なる変数がありますが、まぎらわしいことに全部同じeggsという名前です。3つの変数は次のとおりです。

  • spam()ローカルスコープ内の変数eggs(❶)
  • bacon()ローカルスコープ内の変数eggs(❷)
  • グローバルスコープ内の変数eggs(❸)

この3つの別々の変数が同じ名前ですから、どの変数が使われているかを見極めるのが困難です。スコープが別々でも変数には一意に識別できる名前を付けてください。

グローバル文

関数内からグローバル変数を変更する場合は、global文を使います。global eggsのような行が関数の冒頭にあると、「この関数ではeggsはグローバル変数なのでこの名前のローカル変数を作らないでください」という意味になります。次のコードをglobalStatement.pyという名前で保存してください。

def spam():
  ❶ global eggs
  ❷ eggs = 'spam'

eggs = 'global'
spam()
print(eggs)  # 'spam'が表示される

このコードを実行すると、最後のprint()により次のように表示されます。

spam

spam()関数の冒頭でeggsがglobal文で宣言されていますから(❶)、eggsに'spam'が代入されたときに(➋)、グローバルスコープのeggsに代入されます。ローカル変数のeggsは作成されません。

スコープの識別

以下の4つのルールで、変数がローカル変数なのかグローバル変数なのかを見分けられます。

  1. グローバルスコープの変数(すべての関数の外で使われている変数)は、間違いなくグローバル変数です。

  2. 関数にglobal文があれば、それも間違いなくグローバル変数です。

  3. 関数内にglobal文がなくて変数が代入文で使われていたら、ローカル変数になります。

  4. 関数内にglobal文がなくても代入文で使われていなければ、グローバル変数です。

このルールになじむために、もう一つプログラム例を挙げます。次のコードをsameNameLocalGlobal.pyという名前で保存してください。

def spam():
  ❶ global eggs
    eggs = 'spam'  # グローバル変数

def bacon():
  ❷ eggs = 'bacon'  # ローカル変数

def ham():
  ❸ print(eggs)  # グローバル変数

eggs = 'global'  # グローバル変数
spam()
print(eggs)

spam()関数内では、eggsのglobal文がありますから、eggsはグローバル変数です(➊)。bacon()関数内では、代入文があるので、eggsはローカル変数です(➋)。ham()関数内では、global文はありませんが代入文で使われていないので、eggsはグローバル変数です(➌)。sameNameLocalGlobal.pyを実行すると、出力は次のようになります。

spam

次のプログラムのように、関数内で変数に値を代入する前にその変数(ローカル変数)を使おうとすると、エラーが発生します。次のコードをsameNameError.pyという名前で保存してください。

def spam():
    print(eggs)  # エラー
  ❶ eggs = 'spam local'
   
❷ eggs = 'global'
spam()

このプログラムを実行すると、エラーメッセージが表示されます。

Traceback (most recent call last):
  File "C:/sameNameError.py", line 6, in <module>
    spam()
  File "C:/sameNameError.py", line 2, in spam
    print(eggs)  # エラー
UnboundLocalError: local variable 'eggs' referenced before assignment

spam()関数にはeggsの代入文がありますから(➊)、spam()内のeggsはローカル変数です。しかしeggsに値が代入される前にprint(eggs)を実行しているので、ローカル変数eggsはまだ存在しません。グローバル変数のeggsが参照されることはありません(➋)。

UnboundLocalErrorというエラー名はややわかりづらいです。Pythonでは、(boundの原形である)bindが代入と同じ意味で、代入前にローカル変数を使ったことを示しています。

関数はブラックボックス

関数では入力(パラメータ)と出力値さえわかれば十分なことが多いです。関数のコードがどのように動作するかを必ず理解しなければならないわけではありません。このように抽象化して捉えると、関数はブラックボックスだと言えます。

この考え方は現代のプログラミングの基礎になっています。本書の後半の章ではほかの人が書いたモジュールの関数を紹介します。興味があればソースコードを覗いてみることはできますが、関数を使うのにそこまでする必要はありません。関数ではグローバル変数を使わないことが推奨されていますから、関数が自分の書いたプログラムに干渉する心配は普通しなくてよいです。

例外処理

これまでの知識では、エラー、すなわち例外が発生すると、プログラム全体がクラッシュします。実務で使うプログラムではそれは避けたいでしょう。クラッシュさせずにエラーを検知して、しかるべき処理を行い、プログラムの実行を続けたい場面があるはずです。

例えば、次に紹介するプログラムでは、0で割り算をするとエラーが発生します。次のコードをzeroDivide.pyという名前で保存してください。

def spam(divide_by):
    return 42 / divide_by

print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))

spam関数を定義して、いろいろなパラメータを渡してその関数を実行した結果を表示しています。このプログラムを実行すると、次のように出力されます。

21.0
3.5
Traceback (most recent call last):
  File "C:/zeroDivide.py", line 6, in <module>
    print(spam(0))
  File "C:/zeroDivide.py", line 2, in spam
    return 42 / divide_by
ZeroDivisionError: division by zero

0で割り算しようとするとZeroDivisionErrorが発生します。エラーメッセージ中に示されている行番号から、spam()のreturn文でエラーが発生していることがわかります。

ほとんどの場合、例外が発生するのはバグがコードに混入しているせいで、そのバグを修正する必要があります。しかし、(コードのバグではない何らかの事情により)例外の発生が想定され、例外から回復することが求められる場合もあります。例えば、第10章では、ファイルからテキストを読み取ります。存在しないファイルのファイル名を指定すると、FileNotFoundError例外が送出されます。プログラムをクラッシュさせるのではなく、例外を処理してユーザーにファイル名をもう一度入力してもらう動作を期待するかもしれません。

エラーはtry文とexcept文で処理できます。エラーが発生する可能性のあるコードをtry節に入れます。エラーが発生するとプログラム実行がexcept節に移ります。

先の0で割り算をするコードをtry節に入れ、エラー発生時の処理をするコードをexcept節に書きます。

def spam(divide_by):
    try:
        # このブロックでZeroDivisionErrorが発生してもプログラムはクラッシュしない
        return 42 / divide_by
    except ZeroDivisionError:
        # ZeroDivisionErrorが発生するとこのブロックのコードが実行される
        print('Error: Invalid argument.')

print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))

try節のコードでエラーが発生すると、直ちにプログラム実行はexcept節に移ります。そして、通常どおりプログラム実行が継続します。try節で例外が送出されなければ、except 節は飛ばされます。先のプログラムの出力は次のとおりです。

21.0
3.5
Error: Invalid argument.
None
42.0

try節の関数内で発生するエラーが捕捉されます。spam()関数の呼び出しをtry節のブロックに入れた次のプログラムを見てみましょう。

def spam(divide_by):
    return 42 / divide_by

try:
    print(spam(2))
    print(spam(12))
    print(spam(0))
    print(spam(1))
except ZeroDivisionError:
    print('Error: Invalid argument.')

このプログラムを実行すると、次のように出力されます。

21.0
3.5
Error: Invalid argument.

print(spam(1))が実行されないのは、プログラム実行がexcept節に移るとtry節には戻って来ないからです。except節のあとから通常どおりプログラム実行を続けます。

短いプログラム:ジグザグ

これまでに学んだ内容を活かしてちょっとしたアニメーションプログラムを書いてみましょう。停止ボタンを押すかCTRL-Cキーを押すかして実行を止めるまでジグザグのアニメーションを表示します。このプログラムを実行すると、次のように出力されます。

    ********
   ********
  ********
 ********
********
 ********
  ********
   ********
    ********

次のコードをファイルエディタに入力してzigzag.pyという名前で保存してください。

import time, sys
indent = 0  # インデントさせるスペースの個数
indent_increasing = True  # インデントを増やしていくか減らしていくか

try:
    while True:  # メインプログラムループ
        print(' ' * indent, end='')
        print('********')
        time.sleep(0.1) # 0.1秒停止

        if indent_increasing
            # スペースの個数を増やす
            indent = indent + 1
            if indent == 20:
                # 方向転換
                indent_increasing = False
        else:
            # スペースの個数を減らす
            indent = indent - 1
            if indent == 0:
                # 方向転換
                indent_increasing = True
except KeyboardInterrupt:
    sys.exit()

このコードを最初から一行ずつ見ていきましょう。

import time, sys
indent = 0 # インデントさせるスペースの個数
indent_increasing = True  # インデントを増やしていくか減らしていくか

最初にtimeモジュールとsysモジュールをインポートしています。このプログラムには2つの変数があります。変数indentは8個のアスタリスクの前のスペースの個数で、変数indent_increasingはインデントを増やすか減らすかを示すブール値です。

try:
    while True:  # メインプログラムループ
        print(' ' * indent, end='')
        print('********')
        time.sleep(0.1) # 0.1秒停止

残りのプログラムはtry節に入れています。プログラムの実行中にCTRL-Cが押されると、PythonがKeyboardInterrupt例外を送出します。try-except文がなければ、プログラムはクラッシュして見苦しいエラーメッセージが表示されます。このプログラムでは、sys.exit()を呼び出してKeyboardInterrupt例外をきれいに処理しています。(プログラムの最後のexcept文でその処理をしています。)

while True:の無限ループによりプログラムは永久に繰り返されます。その無限ループ内で ' ' * indentにより、スペースを適当な分量だけ入れています。これらのスペースのあとに改行してほしくありませんから、最初のprint()の呼び出しではend=''を指定しています。2つ目のprint()の呼び出しでアスタリスクを表示します。time.sleep()関数をこれまでに説明していませんでしたが、ここでプログラムが0.1秒停止することがわかれば十分です。

        if indent_increasing:
            # スペースの個数を増やす
            indent = indent + 1
            if indent == 20:
                indent_increasing = False  # 方向転換

それから、次にアスタリスクを表示するときのインデントの量を調整します。indent_increasingがTrueなら、indentを1増やします。インデントが20に達したら、インデントを減らすようにします。

        else:
            # スペースの個数を減らす
            indent = indent - 1
            if indent == 0:
                indent_increasing = True  # 方向転換

indent_increasingがFalseなら、indentを1減らします。インデントが0に達したら、またインデントを増やすようにします。インデントを増やそうが減らそうが、メインのループの最初に戻り、アスタリスクを表示します。

プログラムがtryブロックを実行中にCTRL-Cが押されると、except文がKeyboardInterrrupt例外を送出し、その例外を処理します。

except KeyboardInterrupt:
    sys.exit()

プログラム実行がexceptブロックに移り、sys.exit()を実行して、プログラムを終了します。このように、メインのプログラムが無限ループだとしても、プログラムを終了させる方法があります。

短いプログラム:グラフのスパイク

もう一つアニメーションのプログラムを作ってみましょう。このプログラムでは、文字列の繰り返しと入れ子になったループを使ってグラフのスパイクを描画します。次のコードをspike.pyという名前で保存してください。

import time, sys

try:
    while True:  # メインプログラムループ
        # 長さを増やしながら線を引く
        for i in range(1, 9):
            print('-' * (i * i))
            time.sleep(0.1)

        # 長さを減らしながら線を引く
        for i in range(7, 1, -1):
            print('-' * (i * i))
            time.sleep(0.1)
except KeyboardInterrupt:
    sys.exit()

このプログラムを実行すると、次のようなグラフのスパイクを繰り返し描画します。

-
----
---------
----------------
-------------------------
------------------------------------
-------------------------------------------------
----------------------------------------------------------------
-------------------------------------------------
------------------------------------
-------------------------
----------------
---------
----

一つ前のジグザグプログラムと同様に、スパイクプログラムにはtime.sleep()関数とsys.exit()関数が必要です。最初にtimeモジュールとsysモジュールをインポートしています。tryブロックはKeyboardInterrupt例外を捕捉します。ユーザーがCTRL-Cを押すと例外が送出されます。それまでは無限ループでグラフのスパイクを描画し続けます。

1つ目のforループではスパイク部分を増やしながら描画します。

        # 長さを増やしながら線を引く
        for i in range(1, 9):
            print('-' * (i * i))
            time.sleep(0.1)

変数iは1、2、3と9まで増えていくので(9は含みません)、print()は文字列'-'を1 * 1で1、2 * 2で4、3 * 3で9と増やしながら表示していきます。このコードは、1、4、9、16、25、36、49、64と'-'の長さを増やしていきます。このように指数関数的に増やすことで、グラフのスパイクの前半を描画します。

グラフのスパイクの後半を描画するために、2つ目のforループでは、iを7から始めて1まで減らしていきます(1は含みません)。

        # 長さを減らしながら線を引く
        for i in range(7, 1, -1):
            print('-' * (i * i))
            time.sleep(0.1)

グラフのスパイクの幅を変えたければ、2つのforループ内の9と7の値を調整できます。値を調整してもコードの他の部分はそのままで大丈夫です。

まとめ

関数はコードを論理的に分割する主な方法です。関数内の変数はその関数のローカルスコープ内に存在しますから、ある関数のコードが別の関数の変数の値を直接変更することはできません。そのため、変数の値がどこで変更されたかが限定され、デバッグの労が減ります。

関数を利用するとコードを整理できます。関数はブラックボックスだと考えられます。パラメータの形で入力を受け取り、返り値の形で出力を返します。関数内のコードはほかの関数内の変数に影響を及ぼしません。

これまでの章では、エラーが1回でも発生するとプログラムがクラッシュしました。本章では、try文とexcept文を学習し、エラーを検知してコードの実行を続けられるようになりました。これで一般的なエラーに対するプログラムの耐障害性が高まりました。

練習問題

  1. プログラムで関数を利用するとどのような利点がありますか?

  2. 関数内のコードが実行されるのは、関数が定義されたときか関数が呼び出されたときのどちらですか?

  3. 関数を作成するにはどのような文を書きますか?

  4. 関数と関数呼び出しの違いは何ですか?

  5. Pythonのプログラムにはグローバルスコープがいくつありますか? ローカルスコープはいくつありますか?

  6. 関数の呼び出しが終わったときにその関数のローカルスコープ内の変数はどうなりますか?

  7. 返り値とは何ですか? 返り値を式の一部にすることはできますか?

  8. 関数にreturn文がない場合に、その関数を呼び出したときの返り値はどうなりますか?

  9. どのようにすれば関数内の変数がグローバル変数を参照できますか?

10. Noneのデータ型は何ですか?

11. import areallyourpetsnamederic文は何を行いますか?

12. spamモジュールにbacon()関数があるとします。spamモジュールをインポートしてから、その関数をどのように呼び出しますか?

13. どのようにすればエラーが発生したときにプログラムがクラッシュするのを防げますか?

14. try節とexcept節とは何ですか?

15. 次のコードをnotrandomdice.pyという名前で保存して実行してください。関数呼び出しが同じ数値ばかりを返すのはなぜですか?

import random
random_number = random.randint(1, 6)

def get_random_dice_roll():
    # 1から6までのランダムな整数を返す
    return random_number

print(get_random_dice_roll())
print(get_random_dice_roll())
print(get_random_dice_roll())
print(get_random_dice_roll())

練習プログラム

以下の練習プログラムを書いてください。

コラッツの問題

numberをパラメータとするcollatz()という名前の関数を作成してください。numberが偶数ならcollatz()はnumber // 2(2で割った商)を表示して、その値を返します。numberが奇数なら、collatz()は3 * number + 1(3倍して1を足した数)を表示して、その値を返します。

ユーザーが整数を入力すると、その整数を最初の引数として、返り値が1になるまでcollatz()関数を繰り返し適用するプログラムを書いてください。(驚くべきことに、最初にどのような整数を与えても、この手順で必ず1にたどり着きます。数学者にとってさえもその理由ははっきりしていません。最も単純な解決不能問題とも言われるコラッツの問題です。)

input()の返り値をint()関数で整数に変換するのを忘れないでください。そうしないと文字列値になってしまいます。出力をコンパクトにするために、end=' 'と名前付きパラメータを設定して、print()がすべての値を1行で表示するようにしてください。

このプログラムを実行すると、次のように出力されます。

Enter number:
3
3 10 5 16 8 4 2 1

ヒント:numberは、number % 2 == 0なら偶数で、number % 2 == 1なら奇数です。

入力値の検証

先ほどのプログラムにtry文とexcept文を追加して、ユーザーが整数以外を入力したら検知されるようにしてください。int('puppy')のように整数ではない文字列が渡されると、int()関数はValueErrorエラーを送出します。except節では、整数を入力しなければならない旨のメッセージを表示してください。