0
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型のキーをstr型にしてHashableに入れるとエラー

Last updated at Posted at 2024-06-23

こういうコード↓を書くと、型チェックでエラーが出ます。

from collections.abc import Hashable

def hoge1(arg1: dict[Hashable, str]):
    pass

var1 = { "1": "1" }
hoge1(var1) #=> 型チェックでエラー出る

Argument of type "dict[str, str]" cannot be assigned to parameter "arg1" of type "dict[Hashable, str]" in function "hoge1"
TypeVar "_KT@dict" is invariant
"str" is incompatible with protocol "Hashable"(reportGeneralTypeIssues)

なぜでしょうか。

環境

  • Python 3.10.12
  • Google Colaboratory(Colab)
    • Colab上の型チェックで確認していますが、pyrightでも同じような事が起きるのを確認しています。

補足: typing.Hashable is deprecated

本稿で扱うのはHashableですが、typing.HashableはPython3.12で非推奨となり、collections.abc.Hashableを使うことが推奨されます。本稿で取り上げるエラーの話はどちらでも挙動は変わりません。
typing.Hashablecollections.abc.Hashableのエイリアスなのでどちらを使っても構わなかったのですが、これから使うならばcollections.abc.Hashableを使うのが良いでしょう。

# from typing import Hashable # Python 3.12で非推奨
from collections.abc import Hashable

エラーの概要

Hashableというのは__hash__()メソッドが実装されていることであり、辞書(dictionary)のキーにセットされた時などに利用されます。str型が当然満たす性質です。
通常、Hashable型を指定されているところにstr型の変数は適合します。

from collections.abc import Hashable

def hoge2(arg1: Hashable):
    pass

var2 = "1"
hoge(var2) # エラー出ない

ですが冒頭に挙げた通り、辞書(dictionary)のキーにHashable型を指定されていると、そこへstr型をキーに持つ辞書を渡しても適合せず型チェックでエラーになります。

def hoge1(arg1: dict[Hashable, str]):
    pass

var1 = { "1": "1" }
hoge1(var1) # 型チェックでエラー出る

これは辞書のキー特有ではなく、辞書の値やリスト等に対しても同様のエラーが出ます。

def hoge3(arg1: dict[str, Hashable]):
    pass

var3 = { "1": "1" }
hoge3(var3) # エラー出る
def hoge4(arg1: list[Hashable]):
    pass

var4 = ["1"]
hoge4(var4) # エラー出る

Hashableはその特性上辞書のキーとして使うことが多いような気がしますし、本問題は筆者がLangGraphのadd_conditional_edge()の引数にある辞書のキーpath_map: Optional[Union[dict[Hashable, str], list[str]]]で引っかかった事なので、本稿では辞書のキーを対象として話を進めます。

型チェックでエラーが出る理由

改めてエラーメッセージを見てみます。こんな一文がありました。

TypeVar "_KT@dict" is invariant

「辞書のキーはinvariantだ」と言っています。

invariantというのはvariance(変性)の話をしています。辞書のキーはmutable(可変)なコンテナなので、そこで使われている型はinvariant(非変)になります。
invariantなので、HashableとされていたらHashableだけが当てはまり、strだとエラーになるというわけです。

変性についての詳細は参考文献を参照してください。

対応方法の例

str型で受け入れる

def hoge1(arg1: dict[str, str]):
    pass

var1 = { "1": "1" }
hoge1(var1) # エラー出ない

まず身も蓋もない話ですが、Hashableを使うのをやめてstrで受け入れることでエラーを回避できます。

今回筆者が実際に遭遇したのはライブラリ側に関数定義があるので、このような回避方法は避けたいです。

辞書のキーをHashable型と明示

def hoge1(arg1: dict[Hashable, str]):
    pass

var1: dict[Hashable, str] = { "1": "1" }
hoge1(var1) # エラー出ない

dict[str, str]とせずにHashableであることを明示的に型アノテーションすれば、型が一致するためエラーは回避できます。

Hashableをupper boundとする型変数を定義

from typing import TypeVar
from collections.abc import Hashable

K = TypeVar('K', bound=Hashable)
def hoge1(arg1: dict[K, str]):
    pass

var1 = { "1": "1" }
hoge1(var1) # エラー出ない

K = TypeVar('K', bound=Hashable)とするとエラーは出なくなりました。
この解法はChatGPTに教えてもらったのですが、確かにエラーは消えたものの理由については「型チェッカーに具体的な情報を提供するとエラーを回避できる」というなんだか納得いかない事しか言ってくれませんでした。AIがエラーを直してくれるけど人類にその理解はできないとか、未来のプログラミング感ありますね。

おそらく、strHashableと一致しないためエラーでしたが、「Hashableを上界(upper bound)とする型」にはstrが当てはまるためエラーでなくなったということでしょうか。実質的にどっちも同じことを言っているような気もしますが、Pythonの型チェックの仕様上は異なるようです。

辞書を変数に代入しない

def hoge1(arg1: dict[Hashable, str]):
    pass

hoge1({ "1": "1" }) # エラー出ない

辞書を変数に代入せず直接関数に渡すと、型チェックでエラーは出ませんでした。
なぜでしょうか?理由は推測できますが、仕様がどうなっているのか私にはよく分かりませんでした。

これまで述べた通り、辞書はmutableなのでその変性はinvariant(非変)だから、エラーになります。それは辞書を変数に代入した場合の話です。
変数に代入してないリテラル時点ではimmutableだから多分covariant(共変)になり、dict[str, str]dict[Hashable, str]のサブタイプとみなせるようになるからエラーは出ない、という事なのかなと思いましたが、Pythonの型チェックが実際本当にそういう仕様なのかは分かりません。

参考文献

0
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
0
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?