関数の事前条件と副作用
割算の処理と記録用リストを使い、正常終了とゼロ除算の後の状態を比べます。入力検査を先に行う版へ直し、戻り値、例外、共有状態をそれぞれ確認します。
説明
関数は戻り値を返すだけでなく、渡されたリストなどの状態を変更することがあります。途中で例外が出ても、それより前の変更が自動で戻るとは限りません。
割算の処理と記録用リストを使い、正常終了とゼロ除算の後の状態を比べます。入力検査を先に行う版へ直し、戻り値、例外、共有状態をそれぞれ確認します。
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
def divide_first(n, d, audit):
audit.append(("start", n, d))
result = n / d
audit.append(("done", result))
return resultaudit = []; answer = divide_first(12, 3, audit); print(answer, audit)4.0 [('start', 12, 3), ('done', 4.0)]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
print(answer, audit)4.0 [('start', 12, 3), ('done', 4.0), ('start', 12, 0)]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 resultfor 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 []
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)]returned = log.append(("reviewed",)); print(returned, log)None [('done', 0.0), ('done', 4.0), ('reviewed',)]演習完了