一覧PG-06-3 · 関数の入出力

関数の事前条件と副作用

割算の処理と記録用リストを使い、正常終了とゼロ除算の後の状態を比べます。入力検査を先に行う版へ直し、戻り値、例外、共有状態をそれぞれ確認します。

説明

関数は戻り値を返すだけでなく、渡されたリストなどの状態を変更することがあります。途中で例外が出ても、それより前の変更が自動で戻るとは限りません。

割算の処理と記録用リストを使い、正常終了とゼロ除算の後の状態を比べます。入力検査を先に行う版へ直し、戻り値、例外、共有状態をそれぞれ確認します。

1記録を先に追加する関数

auditは呼出元が持つリストです。計算を始める前と成功後に記録します。

def divide_first(n, d, audit): audit.append(("start", n, d))計算前の変更 result = n / d割算 audit.append(("done", result))成功後の変更 return result呼出元へ返す値

appendは渡された同じリストを変更します。関数の戻り値とは別に、呼出元から見える記録が増えます。

2正常呼出しの戻り値と記録

空の記録から12÷3を実行します。

audit = []呼出元の記録; answer = divide_first(12, 3, audit)正常な割算; print(answer, audit)値と副作用

戻り値4.0と、start・doneの二件が確認できます。関数が何をしたか調べるときは、戻り値だけでなく共有状態の変化も対象にします。

3途中で例外が出る呼出し

同じauditを渡し、分母0で呼び出します。

answer = divide_first(12, 0, audit)途中で失敗する呼出し

計算行でZeroDivisionErrorになります。関数はreturnまで進まないため、新しい戻り値の代入も行われません。先に追加したstartがどうなったか、次に確認します。

4例外前の変更は残る

answerとauditを表示します。

print(answer, audit)失敗後の代入先と共有状態

answerは以前の4.0のままですが、auditには12・0のstartが残っています。Pythonの通常の例外処理は、既に行ったリスト更新を自動で巻き戻しません。

5変更する前に型とゼロを検査する

数値型と分母を先に検査し、計算できた後でdoneだけを追加する関数を定義します。

def divide_checked(n, d, audit): if type(n) not in (int, float) or type(d) not in (int, float)受け付ける型の条件: raise TypeError("numbers required")型違いを拒否 if d == 0:ゼロの検査 raise ValueError("denominator must not be zero")計算前に拒否 result = n / d検査後の計算 audit.append(("done", result))計算後の記録 return result

この例はboolを数値引数として受け付けず、分母0も記録を変更する前に拒否します。これら以外の全失敗が起きないという保証ではなく、指定した検査と変更の順番を明示しています。

6拒否された呼出しの変更範囲

新しい記録に対して、不正な分母を二種類試します。try/exceptで例外を表示し、次の入力も検査します。

for divisor in (0, "3"):二種類の不正入力 log = []毎回空の記録 try: divide_checked(12, divisor, log)検査する関数を呼ぶ except (ValueError, TypeError) as error:想定した例外を捕捉 print(type(error).__name__, str(error), log)拒否理由と未変更の記録

どちらも記録は空です。型や値の条件が成立しなかったことを、別々の例外で表しています。例外を捕捉したこと自体が状態を戻したのではなく、変更する前に拒否した結果です。

7正当な0の結果も受け付ける

分子0、分母3は正常な計算です。続けて12÷3も実行します。

log = []成功記録を初期化; print(divide_checked(0, 3, log)0になる正常計算); print(divide_checked(12, 3, log)もう一つの正常計算); print(log)成功した変更

0.0と4.0が返り、成功記録が二件増えました。0という値そのものを一律に不正とするのではなく、今回の条件は「分母が0でない」です。

8Noneが返っても状態は変わる

リストのappendが返す値と、追加後のリストを確認します。

returned = log.append(("reviewed",))共有リストへの追加と戻り値; print(returned, log)Noneと変更後の状態

appendの戻り値はNoneですが、記録は追加されています。戻り値がないように見える操作でも、副作用がないとは限りません。関数の仕様には戻り値と変更対象の両方を記します。

まとめ

事前条件を変更より先に検査し、途中失敗時の状態を確認します。

戻り値、例外、共有データへの副作用を別々に追います。

操作履歴と解説を振り返れます。

Python 3

1
def divide_first(n, d, audit): audit.append(("start", n, d)) result = n / d audit.append(("done", result)) return result
2
audit = []; answer = divide_first(12, 3, audit); print(answer, audit)
4.0 [('start', 12, 3), ('done', 4.0)]
3
answer = divide_first(12, 0, audit)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "<console>", line 3, in divide_first
ZeroDivisionError: division by zero
4
print(answer, audit)
4.0 [('start', 12, 3), ('done', 4.0), ('start', 12, 0)]
5
def divide_checked(n, d, audit): if type(n) not in (int, float) or type(d) not in (int, float): raise TypeError("numbers required") if d == 0: raise ValueError("denominator must not be zero") result = n / d audit.append(("done", result)) return result
6
for divisor in (0, "3"): log = [] try: divide_checked(12, divisor, log) except (ValueError, TypeError) as error: print(type(error).__name__, str(error), log)
ValueError denominator must not be zero []
TypeError numbers required []
7
log = []; print(divide_checked(0, 3, log)); print(divide_checked(12, 3, log)); print(log)
0.0
4.0
[('done', 0.0), ('done', 4.0)]
8
returned = log.append(("reviewed",)); print(returned, log)
None [('done', 0.0), ('done', 4.0), ('reviewed',)]

演習完了