English

Python の型ヒントを理解したい

仕事で漫然と Python に型ヒントをつけていると、type checker の警告を理解できないことがある。実際に遭遇したケースの一つをかなり単純化して書いてみよう

x: list[float] = [1.0]
y: list[int] = [2]
x = y

これを mypy にチェックさせると1

main.py:3: error: Incompatible types in assignment (expression has type "List[int]", variable has type "List[float]")  [assignment]
main.py:3: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
main.py:3: note: Consider using "Sequence" instead, which is covariant
Found 1 error in 1 file (checked 1 source file)

という、型の不整合を示すエラーが出る。intfloat に代入できるので、当然 list[int]list[float] に代入できるのでは…?と思ってしまうのだが実はそうではないということらしい。2 エラーメッセージを読むと、"List" is invariantConsider using "Sequence" instead, which is covariant とあるが、なんだそれとなってしまった。

流石にこれは良くないと思ったので、読むべきとされていそうな資料を一通り読んでみた。具体的にはこの辺り

これらに目を通すと分かるのだが、Python の型チェックでは、部分的に型がついているプログラムの解析をうまくやる必要があるという点で事情が多少複雑で、そのために gradual typing などという概念が発明されたりしている。しかし今回はそういう深い話には踏み込まず、冒頭の例がなぜダメなのかと言う点に絞って書こうと思う。

なぜエラーが出るのか?

結論から話すと、冒頭のコードで mypy がエラーを出すのは、list[int]list[float]subtype ではないからだ。Subtype は文字通り型の間のヒエラルキーを表すもので、PEP 483 では、 second_type という型が first_type の subtype であることを以下のように定めている

  • every value from second_type is also in the set of values of first_type; and
  • every function from first_type is also in the set of functions of second_type.

一つ目の条件は代入先の型が、代入元の値を持っていなければならないというもので、 intfloat の関係を思い出せばわかりやすいだろう。3 冒頭に述べた list[int]list[float] の例もこの条件は満たしている。

問題になるのは二つ目だ。リストの場合これは、list[float] に作用する関数は、list[int] にも作用できなければならないということを言っている(代入元に作用する関数は、代入先にも作用できなければいけないということ)。しかし、例えば x.append(1.0) という操作を考えてみると、これは list[float] には行えるが、list[int] には行えない(もちろん Python ランタイムでエラーが出るわけではないが、整数型リストに浮動小数を append すると、もはやそれは整数型のリストではなくなる、という意味で不正な処理となる)。

よって、冒頭の例は、“list[int]list[float] の subtype ではないのでエラーが出る“ということになる。

Generic type と variance

この話は generic type とその variance という概念と関係がある。

PEP 483では、型を引数にとって別の具体型を返すもののことを generic type と定義している。これは難しい話ではなく、例えば listint を取って、list[int] を返すので generic type である。他にも dicttuple, Union なども馴染みのある generic type だと思う。

t2 が型 t1 の subtype であるとし、また GenType を好きな generic type であるとしよう。PEP 483 では、GenType[t1]GenType[t2] の subtype 関係によって、generic type を以下のように分類している

  • Covariant, if GenType[t2] is a subtype of GenType[t1] for all such t1 and t2.
  • Contravariant, if GenType[t1] is a subtype of GenType[t2] for all such t1 and t2.
  • Invariant, if neither of the above is true.

これを型の variance と呼ぶ。Covariant なら t1, t2 と同じ subtype 関係を、contravariant なら逆の subtype 関係を持ち、そのような綺麗な関係がない場合は invariant となる。

この用語を使うと冒頭のリストのエラー例の原因は、“list は invariant だから“と表現することもできる。これが mypy のメッセージ、 "List" is invariant だ。

冒頭の問題の解決法

Python のリストが invariant である根本原因はリストがミュータブルであることである。要素の置換や追加ができてしまうので、subtype の二つ目の条件が満たせない。

冒頭の例では、個別の要素を操作しようとはしていないので、イミュータブルな配列としてアノテートすれば十分である。そのような用途として使える型に(mypy が suggest している)collections.abcSequence という型がある

# 以下は mypy のエラーが出ない
x: Sequence[float] = [1.0]
y: Sequence[int] = [2]
x = y

この辺の事情は dict のような他のミュータブルなコンテナ型でも同様で、要素の型の親子関係をそのまま引き継ぐことはできない(invariant である)。dict のイミュータブル版が必要なら、Mapping を使う。この手の情報はどこを参照すれば良いのかあまりわかっていないが、collections.abc のドキュメントを見ると、コンテナ型の抽象基底クラスが列挙されている。

collections.abc
collections.abc — Abstract Base Classes for Containers

Python は duck typing の精神を持つ言語なので、関数の引数には具体型をつけるより、所望のインターフェースを持つ抽象基底クラスを使う方が良いのだろう。4

また、Sequence ではなく、あくまでリストとして扱いたければ、参照ではなくコピーを渡したり

x: list[float] = [1.0]
y: list[int] = [2]
x = list(y)

xUnion で表したり

x: Union[list[float], list[int]] = [1.0]
y: list[int] = [2]
x = y

という手段も考えられる(後者の例が subtype の条件を満たしていることはすぐ確認できる)。

おまけ

Subtype や variance はWikipediaにもページがあるくらいなので、Python に限った話ではなく、コンピュータ・サイエンス一般の概念なのだろう。 例えば Julia でも

julia> Int <: Real
true

であるが(Julia では構造体の親子関係の判定に <: というオペレーターが使える)

julia> Vector{Int} <: Vector{Real}
false

である。これはまさに Vector が invariant だということだ。

ちなみに Julia では Real の子を要素に持つようなベクトルの集合を Vector{<:Real} と書ける。そのため以下は true になる。

julia> Vector{Int} <: Vector{<:Real}
true

おそらく Python でいう Union[list[int], list[float]] のようなものを表現しているのだろう。


1

https://mypy-play.net でブラウザから試せる。

2

もちろんこれは mypy のような static type checker が出すエラーの話であって、冒頭のコードは普通の Python のコードとしては何も問題なく動作する。

3

という言い方は多分不正確で、stack overflow で指摘されているように、float が持つ、is_integer() などを int は持っていない。そのため、上の定義に照らすと、intfloat の subtype ではない。実際、PEP 484 - The numeric tower では float とアノテーションがついたオブジェクトには、int が代入できるよう “shortcut” を導入しようと言っている。同じことが Mypy では duck type compatibility として説明されている。その一方で、PEP 483 では、説明のしやすさのためなのか、intfloat の subtype であるかのような書き振りになっている。ここでもそれにならって、あまり厳密にならず、intfloat の subtype であるとして話を進めていこうと思う。

4

実際 PEP 483 には関数の type annotation について “choose the most general type for every argument, and the most specific type for the return value.” とある