筒井@ryu22eです。2023年5月のassert_
関数、Never
型」
みなさんはassert_
関数、Never
型にはこのようなミスを型チェッカー
本記事では、サンプルコードを交えて実際にassert_
関数、Never
型がどう役立つのか解説します。
なお、型チェッカーはMypy 1.
assert_
関数、Never
型についての公式ドキュメントは以下を参照してください。
「到達しないはずなのにバグのため到達してしまう行」を検出してくれるassert_never
関数
assert_never
関数まず最初にassert_
関数について解説します。
以下のサンプルコードには、色を表す列挙型Color
と、それを色名に変換する関数get_
が定義されています。
get_
関数の中ではColor
型の引数color
を構造的パターンマッチ[1]で検証しています。コメントにも書いているように、Color.
の場合が書かれていません。これは、うっかり書き忘れてバグを埋め込んでしまった前提で読んでください。
このコードを実行してみましょう。print(get_
を呼び出した時点で、get_
関数の中でどのパターンにも該当しない場合のコードcase _ as unreachable:
に到達してAssertionError
を送出します。
$ python colors.py 赤 青 Traceback (most recent call last): File "/****/colors.py", line 24, in <module> print(get_color_name(Color.YELLOW)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/****/colors.py", line 18, in get_color_name raise AssertionError(unreachable) AssertionError: Color.YELLOW
このバグをリリース前に見つけるには、Python 3.Color.
が抜けていた場合はバグを検出することはできません。
こんな時に活躍するのがassert_
関数です。前述のコードのget_
関数でAssertionError
を送出している部分を書き換えて、assert_
関数を使うようにしてみます。
このコードをMypyで型チェックしてみましょう。
$ mypy colors2.py colors2.py:19: error: Argument 1 to "assert_never" has incompatible type "Literal[Color.YELLOW]"; expected "NoReturn" [arg-type] Found 1 error in 1 file (checked 1 source file)
assert_
関数を呼んでいる行がエラーになりました。このように、型チェッカーはassert_
関数が呼ばれている行に到達できるケースがあることを検出すると、その行をエラーとしてくれます。
なお、assert_
関数は実際に実行するとAssertionError
を送出します。
$ python colors2.py 赤 青 Traceback (most recent call last): File "/****/colors2.py", line 25, in <module> print(get_color_name(Color.YELLOW)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/****/colors2.py", line 19, in get_color_name assert_never(unreachable) # ここを変更 ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/****/python3.11/typing.py", line 2462, in assert_never raise AssertionError(f"Expected code to be unreachable, but got: {value}") AssertionError: Expected code to be unreachable, but got: <Color.YELLOW: 3>
Never
型を使った自作関数はassert_never
関数を代替できる
前述の通りassert_
関数は実行するとAssertionError
を送出しますが、別の例外にしたり、例外を送出する以外のコードを書きたい場合もあるでしょう。そういう時は、Never
型を使った自作の関数を定義してassert_
関数の代わりに使うことができます。
前述のcolors2.
を以下のように編集して、assert_
関数の代わりに自作のassert_
関数を使うようにします。
このコードをMypyで型チェックすると、変更前のcolors3.
と同じ箇所でエラーを検出します。
$ mypy colors3.py colors3.py:27: error: Argument 1 to "assert_unreachable" has incompatible type "Literal[Color.YELLOW]"; expected "NoReturn" [arg-type] Found 1 error in 1 file (checked 1 source file)
実行してみると、assert_
関数に書かれた自作の例外UnreachableError
を送出します。
$ python colors3.py 赤 青 Traceback (most recent call last): File "/****/colors3.py", line 33, in <module> print(get_color_name(Color.YELLOW)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/****/colors3.py", line 27, in get_color_name assert_unreachable(unreachable) # ここを変更 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/****/colors3.py", line 16, in assert_unreachable raise UnreachableError(arg) UnreachableError: Color.YELLOW
assert_never
関数、Never
型を使えないケース
assert_
関数、Never
型は型チェッカーと組み合わせることでプログラマーのミスを防いでくれる便利なものですが、無闇に使うと問題が起こることがあります。
以下のコードはFizz Buzzを出力する関数infinite_
を定義しています。
上記のコードはfor文にitertools.
を使っているので、無限にループを繰り返します。そのため、最後の行のassert_
には到達しないはずです。
ところが、型チェッカーを使うと以下のようにエラーを検出してしまいますassert_
関数でもNever
型を使った自作関数でも結果は同じです)。
$ mypy fizzbuzz.py fizzbuzz.py:18: error: Argument 1 to "assert_never" has incompatible type "str"; expected "NoReturn" [arg-type] Found 1 error in 1 file (checked 1 source file)
型チェッカーは到達可能・
前述のcolors2.
、colors3.
では列挙型Color
の定義により構造的パターンマッチで取り得るパターンが3つColor.
、Color.
、Color.
)
この問題は以下の公式ドキュメントにも記載されています。
NoReturn
型とNever
型の関係性
これまでに見てきたMypyのエラーメッセージには"NoReturn"
という文言が含まれています。エラーメッセージを和訳するとNoReturn
です」
NoReturnはtypingモジュールに属する型で、Never
型が登場するよりも前に存在しています。型チェッカーではNoReturn
型とNever
型は同じ意味として扱います。
以下でNoReturn
型とは何なのか、なぜ同じ意味のNever
型があるのかについて説明します。
NoReturn
型は2つの役割を持ちます。
まず1つ目は、raise_
関数は中で例外を送出するだけなので戻り値がありません。こういう関数では戻り値の型としてNoReturn
を指定します。
2つ目はボトム型としての役割です。ボトム型は値を持たない型です。逆に言うと、ボトム型以外の型は値を持ちます。たとえば、関数の引数がstr
型なら"example"
、int
型なら1
のように具体的な値を指定できます。ところが、引数がNoReturn
型の場合はどんな値も渡すことができません。
以下のコードは引数の型がNoReturn
型であるため、どんな値を渡しても型チェッカーではエラーになります。
Mypyで上記のコードを型チェックするとraise_
関数の呼び出し部分ですべてエラーになります。
$ mypy example_noreturn2.py example_noreturn2.py:8: error: Argument 1 to "raise_assertionerror" has incompatible type "str"; expected "NoReturn" [arg-type] example_noreturn2.py:9: error: Argument 1 to "raise_assertionerror" has incompatible type "int"; expected "NoReturn" [arg-type] example_noreturn2.py:10: error: Argument 1 to "raise_assertionerror" has incompatible type "None"; expected "NoReturn" [arg-type] Found 3 errors in 1 file (checked 1 source file)
引数の型がNoReturn
NoReturn
という名前からはボトム型の特徴をイメージしにくいという意見が挙がっていました。そこで、Python 3.Never
が追加されました。
NoReturn
型がボトム型であることはPython 3.Never
型が使用可能な場合はNever
型を使うようにしてください。
NoReturn
型に関する公式ドキュメントにもボトム型として利用する場合はNever
型をつかうべきとの記述があります。
NoReturn
型とNever
型の関係性については以下ドキュメントも参照してください。
また、Never
型が採用されるに至った議論の内容は以下のサイトで確認できます。
型チェッカーの対応状況
主要な型チェッカーでのassert_
関数、Never
型への対応状況を以下に掲載します。
型チェッカー名 | 2023年4月7日時点の最新バージョン | 対応状況 |
---|---|---|
Pyright | 1. |
対応済み |
Mypy | 1. |
対応済み |
Pyre | 0. |
対応していない |
pytype | 2023. |
未対応 |
最後に
タイプヒントや型チェッカーは関数やメソッドの引数指定の間違いを指摘してくれるだけでも嬉しいですが、ロジックの誤りを教えてくれるのは嬉しいですね。この記事をきっかけにタイプヒントの導入を検討する人が増えてくれると幸いです。