pythonで事前条件、事後条件の取り入れる方法の一例

私がよく読んでいる達人プログラマーの本の中に契約による設計という章があります。

こちらの内容をCacapon的な解釈だと受け取るデータ、渡すデータ、内部のデータに条件をつけて、関数やクラスが扱うデータが明確にするための考え方だと思っています。

契約による設計によると、三つの要素があります。

  • 事前条件:渡されるデータに対して設ける条件。f(x)のxは8文字までとか
  • 事後条件:処理が終わった後の返り値に設ける条件。y = f(x) の時、yは-になってはいけないとか
  • クラス不変表明:これはよくわかっていないんですけど、メンバー変数の正しさを保証するための条件だと思っています。例えば、クラスXのプロパティa は常に+の整数値であるなど

基本的にこの条件が守れない場合、例外を投げたり処理を終了することで条件内でしか動作しないことを保証するようにします。こうすることで安全なコードになるのでCacaponも達人プログラマーの考えの中ではかなり好きな部類でコードを書くときは使わせてもらっています。

ただ、言語レベルでサポートされているのは Eiffel やClojureなど一部の言語のみであり、私がよく使うpythonではサポートされていません。

なので、多少片手落ちな部分はありますが、とりあえず概念は理解できている事前条件、事後条件をチェックする関数を作ってみました。クラス不変表明はまた別の日にでも。

conditon.py
def pre(condition:bool, message:str):
    if not condition: raise AssertionError('err:pre: ' + message)


def post(condition:bool, message:str):
    if not condition: raise AssertionError('err:post: ' + message)

コード自体はシンプルですね。ちなみにpreが事前条件、postが事後条件です。*1

conditionに判定を行うような式を入れて、結果がFalseだったらエラーを発行する、という流れになります。messageには失敗した時に表示したいメッセージを設定できるようにしてします。

raise は例外を発行するためのコマンドです。ここではAssetionErrorというエラーが発行されるようになっています。

作成した事前条件、事後条件の動作確認用のサンプルはこちらになります。

add.py
from conditon import pre, post


def add(a, b):
    pre(0 < a < 10, f"aは1~9にしてください a:{a}")
    pre(0 < b < 10, f"bは1~9にしてください b:{b}")

    result = a + b

    post(0 <= result < 10, f"resultは自然数の一桁にしてください result:{result}")
    return result


for a in range(10):
    for b in range(10):
        try:
            print(f"{a} + {b} = {add(a,b)}")
        except AssertionError as e:
            print(e)

こちらのadd関数は入力を1~9しか受け付けず、さらに出力する結果は一桁である必要があるように条件をつけています。

出力結果がこちら

$ python3 app.py                                                                                                                                                                                                      
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: bは1~9にしてください b:0
1 + 1 = 2
1 + 2 = 3
1 + 3 = 4
1 + 4 = 5
1 + 5 = 6
1 + 6 = 7
1 + 7 = 8
1 + 8 = 9
err:post: resultは自然数の一桁にしてください result:10
err:pre: bは1~9にしてください b:0
2 + 1 = 3
2 + 2 = 4
2 + 3 = 5
2 + 4 = 6
2 + 5 = 7
2 + 6 = 8
2 + 7 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:pre: bは1~9にしてください b:0
3 + 1 = 4
3 + 2 = 5
3 + 3 = 6
3 + 4 = 7
3 + 5 = 8
3 + 6 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:pre: bは1~9にしてください b:0
4 + 1 = 5
4 + 2 = 6
4 + 3 = 7
4 + 4 = 8
4 + 5 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:pre: bは1~9にしてください b:0
5 + 1 = 6
5 + 2 = 7
5 + 3 = 8
5 + 4 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:pre: bは1~9にしてください b:0
6 + 1 = 7
6 + 2 = 8
6 + 3 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:post: resultは自然数の一桁にしてください result:15
err:pre: bは1~9にしてください b:0
7 + 1 = 8
7 + 2 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:post: resultは自然数の一桁にしてください result:15
err:post: resultは自然数の一桁にしてください result:16
err:pre: bは1~9にしてください b:0
8 + 1 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:post: resultは自然数の一桁にしてください result:15
err:post: resultは自然数の一桁にしてください result:16
err:post: resultは自然数の一桁にしてください result:17
err:pre: bは1~9にしてください b:0
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:post: resultは自然数の一桁にしてください result:15
err:post: resultは自然数の一桁にしてください result:16
err:post: resultは自然数の一桁にしてください result:17
err:post: resultは自然数の一桁にしてください result:18

実行結果は0+0から9+9までの結果になります。入力が10以上の時は確認で来てませんが、入力が0の時、結果が10以上の時はエラーを吐くようになっていますね。無事に事前条件、事後条件をチェックする機能を作ることができました。何かの参考になったら幸いです。また別のブログでお会いしましょう。







…ここからは余談なのですが、pythonにはassertという判定値がFalseだったらAssertionErrorを出してくれるキーワード文があります。

assert 0 == 1 , "エラー!" # 0==1はFalseなのでAssertionErrorになる

これってまんま、pre,postで判定している式ですし、みた感じと同じなのですが、とある理由でわざわざassert文を使っていません。

その理由なのですが、デバッグフラグがFalseだと実行時にオフになっちゃうんです。

例えば、python3 -0 app.pyのように実行すると、assertキーワードで作成した部分は無視されるのがわかると思います。

python以外の言語でもassertのような機能ってデバッグ専用みたいなニュアンスが多いみたいで、ビルドすると無くなったりする場合もありますので注意です。*2

これはCacaponの考えなので賛否両論は承知の上ですが、デバッグで大丈夫だからとassertのようなチェックをする機能を本番でオフにするのはナンセンスだと考えています。

例えばですが、もしpre,postがassertで作られていた場合でadd関数を本番運用すると、入力が-9だろうが、結果が18だろうがエラーになりません。これって想定外の入力も受け付けるってことになるのでどんな挙動になるか運用している人もプログラム作った人も把握できてないプログラムになってしまうんです、これって危ないですよね?

それなら、すぐエラーになって落ちるプログラムのほうがわかりやすいですし安全です。なので、cacaponはassertは基本的に使わず、さっきのような自作で例外を出すコードを書いています。assert使うのはpytestぐらいでしょうか?

…なんか、書いてたら余談の方が長くなってしまった気がします笑

本日は本当にここまでです、ここまで読んでいただきありがとうございました。

*1:DeepLで翻訳したところpreconditionが事前条件、postconditionが事後条件だったので頭文字のpre,postで作りました。

*2:確かC#で似たようなところで困った記憶があります