「達人プログラマ」を読んでDisign by Contractに興味を持ったので、Pythonではどう書けるのか調べてみたら、最初に出てきたのがPEP316だった。
PEP316のスタイルは契約プログラミングの元祖であるEiffelの仕様を意識している。(と言われてEiffelのコードを見たが、似てるのはキーワードだけ?)
class circbuf:
def __init__(self, leng):
"""Construct an empty circular buffer.
pre: leng > 0
post[self]:
self.is_empty()
len(self.buf) == leng
"""
残念ながら、PEP316のリファレンス実装は2007年で止まっていて、代わりに別の作者によるPyContractsがpypiに登録されている。
こちらはEiffel仕様ではないが、3つのスタイルでDbCをサポートしている。
1つ目はデコレータで契約を定義するスタイル。
from contracts import contract
@contract(a='int,>0', b='int,>0', returns='int')
def my_add(a, b):
return a+b
print(my_add(1,1)) # 普通に実行される
print(my_add(1,1.1)) # ちゃんとクラッシュする
2つ目は関数の引数で定義する。感覚的には自然だが、本番環境ではDbCを外すような場合には影響しそうだ。
from contracts import contract
@contract
def my_add2(a : 'int,>0', b : 'int,>0') -> 'int':
return a+b
print(my_add2(1,1))
print(my_add2(1,1.1))
3つ目はdocstringに書くパターン。つまりPEP316に近い。
from contracts import contract
@contract
def my_add3(a, b):
""" Function description.
:type a: int,>0
:type b: int,>0
:rtype: int
"""
return a+b
print(my_add3(1,1))
print(my_add3(1,1.1))
docstringということで、doctestとセットで書けるのはスッキリしてていいかもしれない。
@contract
def my_add3(a, b):
""" Function description.
:type a: int,>0
:type b: int,>0
:rtype: int
>>> my_add3(1,1.1)
2
"""
return a+b
if __name__ == '__main__':
import doctest
doctest.testmod()
上のint,>0
のところは、カンマ区切りでint,>0,<5,!=3
みたいに書けるし、配列とかも指定できて表現力が高い。
preconditionとpostcondition、そしてinvariantがなくて契約プログラミングと呼んでいいのか微妙かもしれないが、意図しない値が入ってきた時点でクラッシュさせるスタイルは、if文でvalidateするよりスッキリ書けそうだ。