一覧PG-02-3 · 型と変換

静的型検査と実行時の検証

短い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

1
cat bad.py
def twice(quantity: int) -> int:
    return quantity * 2

print(twice("3"))
2
mypy --strict --no-pretty bad.py
bad.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)
3
python3 bad.py
33
4
cat checkout.py
import sys

def total(quantity: int) -> int:
    return quantity * 120

quantity = int(sys.argv[1])
print(total(quantity))
5
mypy --strict --no-pretty checkout.py python3 checkout.py 3
Success: no issues found in 1 source file
360
6
python3 checkout.py three 2>&1 | tail -n 1
ValueError: invalid literal for int() with base 10: 'three'
7
python3 checkout.py -1
-120
8
cat checked.py
import 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))
9
mypy --strict --no-pretty checked.py python3 checked.py 0 python3 checked.py -1 2>&1 | tail -n 1
Success: no issues found in 1 source file
0
ValueError: quantity must be non-negative

演習完了