Tech カテゴリのイメージ

TypeError: unhashable type: 'list' の原因と直し方[Python]

辞書のキーに使ったり、集合に入れたりしたときに出るのがこのエラーになる。

TypeError: unhashable type: 'list'

辞書型を入れたときはunhashable type: 'dict'、集合を入れたときはunhashable type: 'set'と、型名の部分が変わって表示される。原因はどれも同じになる。

ハッシュ可能とはどういうことか

Pythonの辞書と集合は、中身をハッシュ値で管理している。要素を入れた時点でハッシュ値を計算し、その値を使って高速に検索している。

ここで問題になるのが、あとから中身を書き換えられるオブジェクトだ。リストは要素を追加したり書き換えたりできるので、入れたときと後で見たときでハッシュ値が変わってしまう。そうなると、入れたはずのものが見つからなくなる。

そのためPythonは、変更できるオブジェクト(リスト、辞書、集合)をハッシュ不可にしている。

・ハッシュ可能。int、float、str、tuple、frozenset、None など変更できないもの ・ハッシュ不可。list、dict、set など変更できるもの

直し方1. tuple に変換する

いちばん多い解決策になる。リストをタプルにすれば、そのまま辞書のキーや集合の要素にできる。

key = [1, 2, 3]
d = {}
d[key] = "value" # TypeError
d[tuple(key)] = "value" # OK

集合に入れる場合も同じになる。

seen = set()
seen.add(tuple(key))

リストのリストから重複を除きたいときにも使える。

data = [[1, 2], [3, 4], [1, 2]]
unique = list({tuple(x) for x in data})

元の形に戻したければ、あとでリストに変換し直す。

unique = [list(x) for x in {tuple(x) for x in data}]

直し方2. 入れ子のリストは再帰的に変換する

タプルにしても中身にリストが残っていると、やはり落ちる。

key = [1, [2, 3]]
d[tuple(key)] = "x" # 中の [2, 3] が残っているので TypeError

深い構造なら、再帰で変換する関数を用意する。

def freeze(obj):
if isinstance(obj, list):
return tuple(freeze(x) for x in obj)
if isinstance(obj, dict):
return tuple(sorted((k, freeze(v)) for k, v in obj.items()))
if isinstance(obj, set):
return frozenset(obj)
return obj
d[freeze(key)] = "x"

辞書をキーにしたい場合、順序が違っても同じものとして扱いたいなら、上のようにsortedを挟んでおく。

直し方3. 順序が関係ないなら frozenset

要素の並び順を区別したくない場合は、タプルよりもfrozensetが適している。

a = frozenset([1, 2, 3])
b = frozenset([3, 2, 1])
print(a == b) # True

タプルだと(1, 2, 3)(3, 2, 1)は別物になるので、用途に応じて選ぶ。

直し方4. JSONの文字列にする

構造が複雑で、キーとして一意に判別できればよい場合は、文字列にしてしまうのも手になる。

import json
key = {"b": 1, "a": [2, 3]}
d[json.dumps(key, sort_keys=True)] = "value"

sort_keys=Trueを付けないと、辞書の順序が違うだけで別のキーになってしまうので注意する。

pandas で出る場合

pandasでもこのエラーはよく出る。列の中身がリストになっているのが原因になる。

df.drop_duplicates() # リストを含む列があると TypeError
df.groupby("tags").size() # tags 列がリストだと TypeError

該当列をタプルに変換してから処理する。

df["tags"] = df["tags"].apply(tuple)
df.drop_duplicates()

リストを行に展開したいだけならexplodeが使える。

df = df.explode("tags")

どの列が原因かわからないときは、型を確認すると早い。

for col in df.columns:
if df[col].apply(lambda x: isinstance(x, (list, dict, set))).any():
print("問題の列:", col)

自作クラスをキーにしたい場合

自分で定義したクラスは、標準ではidに基づくハッシュを持っているのでキーにできる。ただし__eq__を定義するとハッシュが無効になるため、__hash__も一緒に定義する必要がある。

class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))

データクラスならfrozen=Trueを指定するだけで、__eq____hash__の両方が自動で作られる。

from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int

まとめ

・辞書のキーと集合の要素は、あとから変更できないオブジェクトである必要がある。リスト、辞書、集合は使えない ・基本の対処はtuple()への変換。入れ子がある場合は再帰的に変換する ・並び順を区別したくないならfrozensetを使う ・pandasでは列の中身がリストになっているのが原因。apply(tuple)explodeで解決する ・自作クラスで__eq__を定義したら__hash__も定義する。データクラスならfrozen=Trueが簡単