一覧PG-14-2 · 型の抽象化と構成

ジェネリック型と結果の型表現

小さな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)型に応じた情報を表示) PYCODE

0は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)表示名と長さ) PYCODE

Noneは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

1
cat generic_box.py
from 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")
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))"
Success: no issues found in 1 source file
5 AYA
True
3
cat bad_box.py mypy --strict --no-pretty bad_box.py
from 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)
4
cat results.py
from 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.message
5
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)) PYCODE
Success: no issues found in 1 source file
'0' Success value=0
'7' Success value=7
'bad' Failure error=invalid integer
6
cat bad_union.py mypy --strict --no-pretty bad_union.py
from 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)
7
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)) PYCODE
def 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' 3
8
mypy --strict --no-pretty bad_optional.py mypy --strict --no-pretty nullable.py
bad_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
9
cat guards.py
from 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)
10
cat bad_guard.py mypy --strict --no-pretty bad_guard.py mypy --strict --no-pretty guards.py
from 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 file
11
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' str
'abc' 'ABC' str

演習完了