ジェネリック型と結果の型表現
小さなPythonファイルをmypyで検査し、実行結果も確認します。入れ物の型、成功と失敗の候補、不在を表すNone、条件判定による型の絞込みを順に扱います。
説明
型注釈は「どの値を受け取り、何を返すか」を記します。同じ入れ物でも中身の型を対応させたい場合や、成功と失敗で持つ情報が異なる場合には、型の組合せでその関係を表せます。
小さなPythonファイルをmypyで検査し、実行結果も確認します。入れ物の型、成功と失敗の候補、不在を表すNone、条件判定による型の絞込みを順に扱います。
1中身の型を引数にする
Boxは中身の型をTとして受け取り、getで同じ型の値を返します。
cat generic_box.py型引数と保存値・戻り値の対応Box[int]なら保存値とgetの結果がint、Box[str]ならstrという対応を型検査へ伝えます。異なる型ごとに同じクラスを書き直す必要はありません。
2型引数と実行時のクラス
型検査の後で値とクラスの同一性を確認します。
mypy --strict --no-pretty generic_box.py型引数に沿った利用を検査
python3 -c "from generic_box import number, name; print(number.get() + 2, name.get().upper()中身に応じた操作); print(type(number) is type(name)実行時のクラスを比較)"整数の計算は5、文字列の大文字化はAYAです。実行時のクラスはどちらもBoxなのでTrueです。型引数は静的な対応関係を表し、それぞれ別のPythonクラスを自動生成するわけではありません。
3型引数に反する利用
整数のBoxへ文字列を渡し、整数の戻り値へupperを呼ぶコードを検査します。
cat bad_box.py型に反する二つの操作
mypy --strict --no-pretty bad_box.py引数と戻り値の利用を検査生成時の引数の不一致と、intにupperがないことの二つが指摘されます。入れ物に型を付けることで、取り出した後の利用も検査できます。
4成功と失敗を異なる型で表す
Successは整数value、Failureはmessageを持ちます。Resultはそのどちらかを表します。
cat results.py候補の型と型に応じた処理parseは整数変換に成功するとSuccess、失敗するとFailureを返します。describeはisinstanceで成功側を判定し、その経路だけでvalueを参照します。残る経路はFailureなのでmessageを使えます。
50も成功として扱う
成功値0と通常の成功値、変換失敗を同じ関数で処理します。
mypy --strict --no-pretty results.py型ごとの処理を検査
python3 - <<'PYCODE'
from results import parse, describe
for text in ("0", "7", "bad"):
result = parse(text)成功または失敗を返す
print(repr(text), type(result).__name__, describe(result)型に応じた情報を表示)
PYCODE0はSuccessとしてvalue=0になります。値の真偽で成功・失敗を判定しないため、有効な0を失敗に混ぜません。失敗時には値の代わりにエラーの説明を持ちます。
6候補の全てにない属性
SuccessとFailureの候補を絞らず、valueを参照するコードを検査します。
cat bad_union.py候補を絞らず参照するコード
mypy --strict --no-pretty bad_union.py候補ごとの属性を検査Failureにはvalueがないため、union-attrの指摘が出ます。候補の型を並べるだけでは、その全てに同じ属性が生えるわけではありません。
7不在と空文字列
nullable.pyは文字列またはNoneを受け取り、不在のときだけguestへ置き換えます。
cat nullable.py不在を先に扱うコード
python3 - <<'PYCODE'
from nullable import display_name, length
for value in (None, "", "Aya"):不在・空・通常の文字列
print(repr(value), repr(display_name(value)), length(value)表示名と長さ)
PYCODENoneはguest、空文字列は空文字列のままです。lengthの出力はどちらも0ですが、同じ入力という意味ではありません。str | Noneは値にNoneを許す指定であり、引数の省略を許す指定ではありません。
8Noneを除外せず長さを取得
型の候補にNoneが残ったままlenへ渡すコードと、先に除外したコードを検査します。
mypy --strict --no-pretty bad_optional.py不在を除外していないコード
mypy --strict --no-pretty nullable.py不在を先に処理するコードbad_optional.pyはlenが受け取れないNoneを含むため指摘されます。nullable.pyではNoneの経路をreturnで終えており、その後は文字列として扱えます。
9別の判定関数へ型情報を渡す
判定を関数へ切り出す場合、単にboolを返すだけの関数とTypeGuardを付けた関数を比較します。
cat guards.pyboolとTypeGuardの判定関数TypeGuard[str]は、Trueなら引数がstrだという情報を型検査器へ伝えます。ここではisinstanceがその判定を実装しています。通常のboolという注釈だけでは、呼出し側へ同じ型の情報が伝わりません。
10判定後の操作を型検査する
boolを返すis_textを使った場合と、TypeGuardを使った場合を検査します。
cat bad_guard.pyboolの判定を使うコード
mypy --strict --no-pretty bad_guard.py候補が残る場合
mypy --strict --no-pretty guards.pyTypeGuardで絞る場合bool版ではintの候補が残り、upperの利用が指摘されます。TypeGuard版はTrueの経路でstrへ絞られます。ただし、型検査器はこの宣言を信頼するため、判定関数が誤ってTrueを返すと安全性も崩れます。
11実際の値を各経路へ渡す
整数と文字列をnormalizeへ渡し、分岐した後の結果を確認します。
python3 - <<'PYCODE'
from guards import normalize
for value in (4, "abc"):
result = normalize(value)型の判定後に変換
print(repr(value), repr(result), type(result).__name__返り値と実際の型)
PYCODE整数4は文字列"4"へ、文字列abcは大文字のABCへ変換されます。どちらの経路もstrを返すという戻り値の型と、実際の値が対応しています。
まとめ
型引数は保存する値と取り出す値の型を対応させます。
和型では候補ごとに使える属性が異なります。条件分岐で候補を絞ってから必要な操作を行います。
None、空文字列、成功値0は意味が異なります。型と分岐でそれぞれの扱いを明示します。
操作履歴と解説を振り返れます。
シェル / Python・mypy
cat generic_box.pyfrom typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
def get(self) -> T:
return self.value
number = Box[int](3)
name = Box[str]("Aya")mypy --strict --no-pretty generic_box.py
python3 -c "from generic_box import number, name; print(number.get() + 2, name.get().upper()); print(type(number) is type(name))"Success: no issues found in 1 source file 5 AYA True
cat bad_box.py
mypy --strict --no-pretty bad_box.pyfrom generic_box import Box, number
wrong = Box[int]("3")
print(number.get().upper())
bad_box.py:2: error: Argument 1 to "Box" has incompatible type "str"; expected "int" [arg-type]
bad_box.py:3: error: "int" has no attribute "upper" [attr-defined]
Found 2 errors in 1 file (checked 1 source file)cat results.pyfrom dataclasses import dataclass
@dataclass(frozen=True)
class Success:
value: int
@dataclass(frozen=True)
class Failure:
message: str
Result = Success | Failure
def parse(text: str) -> Result:
try:
return Success(int(text))
except ValueError:
return Failure("invalid integer")
def describe(result: Result) -> str:
if isinstance(result, Success):
return "value=" + str(result.value)
return "error=" + result.messagemypy --strict --no-pretty results.py
python3 - <<'PYCODE'
from results import parse, describe
for text in ("0", "7", "bad"):
result = parse(text)
print(repr(text), type(result).__name__, describe(result))
PYCODESuccess: no issues found in 1 source file '0' Success value=0 '7' Success value=7 'bad' Failure error=invalid integer
cat bad_union.py
mypy --strict --no-pretty bad_union.pyfrom results import Result
def describe(result: Result) -> str:
return "value=" + str(result.value)
bad_union.py:4: error: Item "Failure" of "Success | Failure" has no attribute "value" [union-attr]
Found 1 error in 1 file (checked 1 source file)cat nullable.py
python3 - <<'PYCODE'
from nullable import display_name, length
for value in (None, "", "Aya"):
print(repr(value), repr(display_name(value)), length(value))
PYCODEdef display_name(value: str | None) -> str:
if value is None:
return "guest"
return value.upper()
def length(value: str | None) -> int:
if value is None:
return 0
return len(value)
None 'guest' 0
'' '' 0
'Aya' 'AYA' 3mypy --strict --no-pretty bad_optional.py
mypy --strict --no-pretty nullable.pybad_optional.py:2: error: Argument 1 to "len" has incompatible type "str | None"; expected "Sized" [arg-type] Found 1 error in 1 file (checked 1 source file) Success: no issues found in 1 source file
cat guards.pyfrom typing import TypeGuard
def is_text(value: int | str) -> bool:
return isinstance(value, str)
def is_text_guard(value: int | str) -> TypeGuard[str]:
return isinstance(value, str)
def normalize(value: int | str) -> str:
if is_text_guard(value):
return value.upper()
return str(value)cat bad_guard.py
mypy --strict --no-pretty bad_guard.py
mypy --strict --no-pretty guards.pyfrom guards import is_text
def normalize(value: int | str) -> str:
if is_text(value):
return value.upper()
return str(value)
bad_guard.py:5: error: Item "int" of "int | str" has no attribute "upper" [union-attr]
Found 1 error in 1 file (checked 1 source file)
Success: no issues found in 1 source filepython3 - <<'PYCODE'
from guards import normalize
for value in (4, "abc"):
result = normalize(value)
print(repr(value), repr(result), type(result).__name__)
PYCODE4 '4' str 'abc' 'ABC' str
演習完了