静的型検査と実行時の検証
短いPythonファイルをmypyで検査してから実行します。型の不一致、数字でない入力、負数の個数を順に試し、どこで何を検出できるか確認します。
説明
型注釈は、関数が受け取る値や返す値の意図を記します。Pythonは注釈を書いただけで入力を変換・拒否しません。mypyなどの静的検査と、実行時の値の検査は役割が異なります。
短いPythonファイルをmypyで検査してから実行します。型の不一致、数字でない入力、負数の個数を順に試し、どこで何を検出できるか確認します。
1引数と戻り値の型注釈
bad.pyでは個数をintとして受け取り、2倍したintを返す意図を注釈に書いています。呼出し側が渡す値も確認します。
cat bad.py注釈と呼出しを表示quantity: intが引数、-> intが戻り値の注釈です。ところが呼出し側は引用符付きの"3"、つまりstrを渡しています。
2実行前に型検査
mypy 2.4.1で、注釈のある関数と呼出しを検査します。
mypy --strict --no-pretty厳格な型検査・簡潔な表示 bad.py検査対象引数にstrを渡しているがintが期待される、というarg-typeのエラーが出ます。型検査はこのファイルの計算処理を実行せず、式の型と注釈を照合します。
3型エラーがあるファイルを実行
同じファイルをPythonで実行します。
python3 bad.py注釈のあるコードを通常実行実行時には文字列の*が使われ、33と表示されます。型検査の指摘があってもPythonの通常実行は自動では止まりません。この例は例外にならず、意図と異なる結果が出るケースです。
4入力を整数へ変換する
checkout.pyではコマンドライン引数をintへ変換してから料金計算へ渡します。sys.argv[1]は最初の引数を文字列として保持します。
cat checkout.py変換を加えたコードintが返す値をtotalへ渡すため、型の組合せは整います。ここでは120円×個数を求めていますが、変換できない文字列や負数にどう対応するかは別の問題です。
5変換後の型検査と正常入力
同じファイルを検査し、個数3で実行します。
mypy --strict --no-pretty checkout.py修正後を型検査
python3 checkout.py 3文字列3を引数にして実行型検査は成功し、実行結果は360です。型の確認と、実際に渡した入力での動作確認を両方行いました。
6数値でない入力
同じコードへthreeを渡します。末尾の診断行だけを表示します。
python3 checkout.py three変換できない入力 2>&1 | tail -n 1例外の最終診断行を表示intの変換時にValueErrorになりました。型検査では引数がstrであることは分かっても、実行時に渡される全ての文字列が数字かどうかまでは保証しません。
7型は正しくても個数として不適切
今度は-1を入力します。整数への変換は成功します。
python3 checkout.py -1負数を数量へ渡す結果は-120です。型がintであることと、個数として0以上であることは別の条件です。料金計算のルールを型注釈だけで表したつもりにならないようにします。
8数量の条件をコードにする
checked.pyは負数を明示的に拒否します。
cat checked.py0以上という条件を追加したコードif quantity < 0で業務上の条件を調べ、違反したらValueErrorを送出します。型注釈に加えて、値の範囲を実行時に検査しています。
9型検査と境界値を再確認
条件を追加したコードを型検査し、0と-1を実行します。
mypy --strict --no-pretty checked.py条件追加後の型検査
python3 checked.py 0下限の正常値
python3 checked.py -1 2>&1 | tail -n 1下限より小さい値の拒否型検査は成功、0個なら0円、-1なら明示したエラーです。型検査は型の整合性を確認し、入力変換と範囲検査は実際に渡された値を確認します。両者を組み合わせて意図する動作に近付けます。
まとめ
型注釈は通常実行で自動的な変換や拒否を行いません。
静的な型検査、文字列の変換、値の範囲の検査をそれぞれ実施します。
操作履歴と解説を振り返れます。
シェル / Python・mypy
cat bad.pydef twice(quantity: int) -> int:
return quantity * 2
print(twice("3"))mypy --strict --no-pretty bad.pybad.py:4: error: Argument 1 to "twice" has incompatible type "str"; expected "int" [arg-type] Found 1 error in 1 file (checked 1 source file)
python3 bad.py33
cat checkout.pyimport sys
def total(quantity: int) -> int:
return quantity * 120
quantity = int(sys.argv[1])
print(total(quantity))mypy --strict --no-pretty checkout.py
python3 checkout.py 3Success: no issues found in 1 source file 360
python3 checkout.py three 2>&1 | tail -n 1ValueError: invalid literal for int() with base 10: 'three'
python3 checkout.py -1-120
cat checked.pyimport sys
def total(quantity: int) -> int:
if quantity < 0:
raise ValueError("quantity must be non-negative")
return quantity * 120
quantity = int(sys.argv[1])
print(total(quantity))mypy --strict --no-pretty checked.py
python3 checked.py 0
python3 checked.py -1 2>&1 | tail -n 1Success: no issues found in 1 source file 0 ValueError: quantity must be non-negative
演習完了