静かなる名辞

pythonとプログラミングのこと


【python】dictのkeyとvalueを入れ替える話色々

 dictのkeyとvalueを入れ替えたい、誰でも一度はそう思うのではないだろうか。

 方法は何種類かある。それらについて説明する。

 あと、この話は簡単なようで実は罠の宝庫。その辺もちょっと説明しておく。

 目次

スポンサーリンク


for文で書く

 誰でも思いつく方法。

>>> d = {"a":"あ","i":"い"}
>>> result = {}
>>> for k,v in d.items():
...     result[v] = k
... 
>>> result
{'あ': 'a', 'い': 'i'}

 d.items()は次のようなものを返す。

>>> d.items()
dict_items([('a', 'あ'), ('i', 'い')])

 dict_itemsは辞書ビューオブジェクト。詳細については公式ドキュメントを確認してほしい。形としては、中のtupleのlist通りのものが返るので、forで素直に処理すれば良い。これが基本中の基本。

リスト内包表記で書く

 上でも触れたように、d.items()で辞書ビューオブジェクトが返る。

>>> d = {"a":"あ","i":"い"}
>>> d.items()
dict_items([('a', 'あ'), ('i', 'い')]) 
# こんな形 -> [('a', 'あ'), ('i', 'い')]

 実は、この形のiterableをdictのインスタンス生成時に渡してやると、ちゃんと元の辞書になる。

>>> dict(list(d.items())) # 1回リストにしてみても大丈夫!
{'a': 'あ', 'i': 'い'}

 これを応用して、次のように書くことができる。

>>> dict([(v,k) for k,v in d.items()])
{'a': 'あ', 'i': 'い'}

 まあ、単にkeyとvalueを入れ替えたいだけなら次の辞書内包表記の方が気軽だけど。この方法は2つのリストがあって、合体させて辞書にしたいみたいなとき、zipと組み合わせて使うと威力を発揮したりする。

辞書内包表記

 なんとこんな簡単に書ける。

>>> {v:k for k,v in d.items()}
{'a': 'あ', 'i': 'い'}

 辞書内包表記という。なんだか不思議だけど、こうやって書くことができる。

罠の説明

 基本的な方法は上で説明した通りなのだけど、この話にはたくさん罠があるので、それについて説明する。

罠1:valueがmutableだとできない(TypeError: unhashable type: ~)

 mutableとは値(内部状態)を変更できるオブジェクトのこと。

 普段はあまり意識しない概念だが、たとえばpython世界では数字の1はmutableではなくimmutableで、数字の1のオブジェクトを上書きしようとしても2になったり3になったりは絶対にしない。1+1という演算を行うと、新しく数字の2のオブジェクトが生成されて返される*1

 一方、たとえばlistでは要素をappendしても同じオブジェクトのまま内部状態が上書きされて、結果的に要素が1つ増える。こういうのがmutable*2

 immutableなオブジェクトの例を挙げると、

  • int
  • float
  • str
  • tuple

 こんな連中がそう。

 mutableなオブジェクトの例は、

  • list
  • set
  • dict

 大体イメージが掴めただろうか?

 さて、辞書のkeyとvalueを入れ替える話に戻る。

 結論から言うと、mutableなオブジェクトは辞書のkeyにはできない。たとえば、listをkeyに使おうとするとこんなエラーが返ってくる。

>>> {[1,2,3]:1}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

 そりゃ、辞書のキーがほいほい書き換わったら困るもんな。だけど、valueにはmutableは普通に使えるので、

>>> d = {123:[1,2,3]}
>>> {v:k for k,v in d.items()}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <dictcomp>
TypeError: unhashable type: 'list'

 こんな悲しい目に遭う。対策は適当なimmutableに変換してやること。リストならtupleにするのが良いだろう。

>>> d = {123:[1,2,3]}
>>> {tuple(v):k for k,v in d.items()}
{(1, 2, 3): 123}

 じゃあvalueもdictだったらどうしよう? リストとstrが入り混じってたりしたら? そんなややこしいdictのkeyとvalueを入れ替えようとするくらいなら、処理フローを抜本的に見直した方が良いだろう。絶対もっとマシな書き方がある。

罠2:複数のkeyが同じvalueに刺さってるとき(エラーは出ないが悲しい結果になる)

 さて、ここまで説明してきたような方法は、実はkeyとvalueが1対1で対応しているときしか使えない。数学用語でいう単射。

 複数のvalueが同じkeyに刺さってる場合・・・は、まあ、dictの性質上あり得ないが、複数のkeyが同じ値に刺さる場合はままある。これのkeyとvalueを入れ替えてひっくり返したからといって、dictの性質上、複数のvalueが同じkeyに刺さることはあり得ない。どうなるのか?

>>> d = {"あ":"a", "い":"i", "ア":"a", "イ":"i"}
>>> {v:k for k,v in d.items()}
{'a': 'あ', 'i': 'イ'}

 いずれかの値1つに決まる。こういうのはロジックエラーの温床になって怖いけど、辞書の仕様上仕方ない。

 ちなみにどれに決まるかは、ドキュメントでも定義されておらずランダムだと思う*3

 これの解決方法は、どんな形で結果を得たいかによって変わってくる。普通は何らかのcollectionにvalues(入れ替える前のkeys)を格納してあげたいことが多いと思う。その場合は、こんな風に書ける。簡潔に書くためにdefaultdictを使い、default_factory(要するにデフォルト値を生成するもん)にはsetを使ってみた。一応defaultdictのドキュメントを貼っておく。

8.3. collections — コンテナデータ型 — Python 3.3.6 ドキュメント

>>> from collections import defaultdict
>>> d = {"あ":"a", "い":"i", "ア":"a", "イ":"i"}
>>> result = defaultdict(set)
>>> for k,v in d.items():
...     result[v].add(k)
... 
>>> result
defaultdict(<class 'set'>, {'a': {'ア', 'あ'}, 'i': {'い', 'イ'}})

 こればっかりは内包表記で書こうとしても上手く行かない。for文を回して愚直に付け加えていく必要がある(と思う)。

まとめ

 意外と考えることが多くて面倒くさい。安直に入れ替えようとする前に、keyとvalueを入れ替えて本当に大丈夫かどうかは良く検討する必要はある。自分で構造をよく理解していないような辞書に対してやってはいけない。

*1:実際には高速化のためにオブジェクトをキャッシュしたり、色々忙しいことをしているらしいが

*2:厳密な説明じゃないと思うが、ご了承いただきたい

*3:でも何回回しても同じ結果になるので、内部的には「順番」はあるんだと思う。たぶん最後に入ることになる値