1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Python] dictやlistの中身がUnion型の場合のmypyエラーの原因と対応策

Last updated at Posted at 2024-09-26

問題

関数の引数として中身がUnion型のdictlistを定義した場合に、関数の呼び出し部分でmypyの型エラーが発生しました。直感的にはこれらの型は包含関係にありそうですが、どうやらdictやlistの型は奥が深いようです。

def func_dict(dic: dict[int|str, str]) -> None:
    pass

def func_list(ls: list[int|str]) -> None:
    pass

dic = {'a': 'b'}
ls = ['a', 'b']
func_dict(dic)
func_list(ls)

error: Argument 1 to "func_dict" has incompatible type "dict[str, str]"; expected "dict[int | str, str]" [arg-type]
error: Argument 1 to "func_list" has incompatible type "list[str]"; expected "list[int | str]" [arg-type]

原因

結論から言うと、strint|strなど中身の型自体に包含関係があっても、それらを要素とするdictlistなどの invariantなGenericsどうしは包含関係を失うことが原因です。以降、この詳細を解説していきます。

Genericsについて

まず、PythonのdictlistはGenericsを用いて表現されているコンテナの1つです。Genericsとは、データ型を表現する型の一部を変数化する機能です。これにより、要素に様々な型を定義できるようになっています。type hintを書くときにlist[]のように[]を用いてかかれる型がGenericsにあたります。このコンテナを扱う上での大きな注意点は、中身の型のsubtype関係を必ずしも引き継がないことです。具体的には、型Aが型Bのsubtype (以降 A ⊂ B とかく)だった場合でも、必ずしもGenerics[A] ⊂ Generics[B]ではないということです。この点が今回のエラーの発生要因となってくるので、以下で詳しく解説していきます。

Genericsのsubtype関係

PEP483[1] によるとGenericsどうしのsubtype関係は以下の3つに分類されるそうです。

  • covariance (共変)
    • 中身のサブタイプの親子関係を受け継ぐ
      -> A ⊂ Bのとき Generics[A] ⊂ Generics[B]
    • 例: Sequence, Union, FrozensSet
  • contravariance (反変)
    • 中身のサブタイプの親子関係を逆転した関係になる
      -> A ⊂ Bのとき Generics[A] ⊃ Generics[B]
    • 例: Callable の argument type
  • invariance (非変)
    • 中身のサブタイプ関係を完全に引き継がない
      -> A ⊂ B のときGenerics[A] ⊂ Generics[B]でもGenerics[A] ⊃ Generics[B] でもない
    • 例: List, Dict

基本的に Sequence 等のimmutableな (値の追加や置換ができない)インターフェースを持つデータ型はcovarianceですが、dictlistなどmutableなデータ型はinvarianceとなっています。strint|strは subtype関係 ではないですが、同様の仕組みで、dict等の箱に入れることによって包含関係が失われると考えるのが自然だと思います。

解決方法の例

1. 引数に入れる変数の型を明示する[2]

引数に代入する変数の型を dict[int|str, str] などとアノテーションしてしまえば、関数で定義した型と一致するためエラーがでることはありません。ただ関数を呼び出す度に、型ヒントを確認したり型エイリアスをインポートする手間がかかるのがデメリットです。

dic : dict[int|str, str] = {'a': 'b'}
ls : list[int|str] = ['a', 'b']
func_dict(dic)
func_list(ls)

2.dictやlist自体のUnion型で定義する

Union はcovarianceなGenericsなので、中身でなくlistdict自体のUnion型で定義してしまえばmypyは通ります。ただ、とりうる型のバリエーションが増えると、かなり冗長になってしまいます。

def func_dict(dic: dict[int, str] | dict[str, str]) -> None:
    pass
def func_list(ls: list[int] | list[str]) -> None:
    pass

3.中身の型を自作のGenericsで定義する

int|str は invariantな箱の中では int や str そのものとは違う型として認識されてしまう一方で、 TypeVar('T', str, int) という自作Genericsは、intstrそのものになれる変幻自在の型を意味するため、dictの中であってもmypyエラーが起きません。

from typing import TypeVar

T = TypeVar('T', str, int)

def func_dict(dic: dict[T, str]) -> None:
    pass
def func_list(ls: list[T]) -> None:
    pass

4. dictやlistをcovariantなGenericsに置き換える[2]

dictlistの抽象基底クラスであるMappingSequence型はcovariantであるため、それに置き換えれば問題は解決します。しかし、それらは同時にimmutableもであるため、関数内で要素を追加・削除したい場合にはこの方法は使えません。

from typing import Mapping, Sequence

def func_dict(dic: Mapping[str|int, str] -> None:
	pass
def func_list(ls: Sequence[str|int]) -> None:
    pass

参考

[1] PEP483 - The Theory of Type Hints, 'Covariance and Contravariance', https://peps.python.org/pep-0483/#covariance-and-contravariance
[2] mypy - Common issues and solutions, 'Invariance vs covariance', https://mypy.readthedocs.io/en/stable/common_issues.html#invariance-vs-covariance

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?